Skip to main content

azul_layout/
window.rs

1//! Window layout management for solver3/text3
2//!
3//! This module provides the high-level API for managing layout
4//! state across frames, including caching, incremental updates,
5//! and display list generation.
6//!
7//! The main entry point is `LayoutWindow`, which encapsulates all
8//! the state needed to perform layout and maintain consistency
9//! across window resizes and DOM updates.
10
11use std::{
12    collections::{BTreeMap, BTreeSet},
13    sync::{
14        atomic::{AtomicUsize, Ordering},
15        Arc,
16    },
17};
18
19use azul_core::{
20    animation::UpdateImageType,
21    callbacks::{FocusTarget, HidpiAdjustedBounds, IFrameCallbackReason, Update},
22    dom::{
23        AccessibilityAction, AttributeType, Dom, DomId, DomIdVec, DomNodeId, NodeId, NodeType, On,
24    },
25    events::{EasingFunction, EventFilter, FocusEventFilter, HoverEventFilter},
26    geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition},
27    gl::OptionGlContextPtr,
28    gpu::{GpuScrollbarOpacityEvent, GpuValueCache},
29    hit_test::{DocumentId, ScrollPosition, ScrollbarHitId},
30    refany::{OptionRefAny, RefAny},
31    resources::{
32        Epoch, FontKey, GlTextureCache, IdNamespace, ImageCache, ImageMask, ImageRef, ImageRefHash,
33        OpacityKey, RendererResources,
34    },
35    selection::{
36        CursorAffinity, GraphemeClusterId, Selection, SelectionAnchor, SelectionFocus,
37        SelectionRange, SelectionState, TextCursor, TextSelection,
38    },
39    styled_dom::{
40        collect_nodes_in_document_order, is_before_in_document_order, NodeHierarchyItemId,
41        StyledDom,
42    },
43    task::{
44        Duration, Instant, SystemTickDiff, SystemTimeDiff, TerminateTimer, ThreadId, ThreadIdVec,
45        ThreadSendMsg, TimerId, TimerIdVec,
46    },
47    window::{CursorPosition, MonitorVec, RawWindowHandle, RendererType},
48    FastBTreeSet, FastHashMap,
49};
50use azul_css::{
51    css::Css,
52    props::{
53        basic::FontRef,
54        property::{CssProperty, CssPropertyVec},
55    },
56    AzString, LayoutDebugMessage, OptionString,
57};
58use rust_fontconfig::FcFontCache;
59
60#[cfg(feature = "icu")]
61use crate::icu::IcuLocalizerHandle;
62use crate::{
63    callbacks::{
64        CallCallbacksResult, Callback, ExternalSystemCallbacks, FocusUpdateRequest, MenuCallback,
65    },
66    managers::{
67        gpu_state::GpuStateManager,
68        iframe::IFrameManager,
69        scroll_state::{ScrollManager, ScrollStates},
70    },
71    solver3::{
72        self, cache::LayoutCache as Solver3LayoutCache, display_list::DisplayList,
73        layout_tree::LayoutTree,
74    },
75    text3::{
76        cache::{
77            FontManager, FontSelector, FontStyle, InlineContent, LayoutCache as TextLayoutCache,
78            LayoutError, ShapedItem, StyleProperties, StyledRun, TextBoundary, UnifiedConstraints,
79            UnifiedLayout,
80        },
81        default::PathLoader,
82    },
83    thread::{OptionThreadReceiveMsg, Thread, ThreadReceiveMsg, ThreadWriteBackMsg},
84    timer::Timer,
85    window_state::{FullWindowState, WindowCreateOptions},
86};
87
88// Global atomic counters for generating unique IDs
89static DOCUMENT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
90static ID_NAMESPACE_COUNTER: AtomicUsize = AtomicUsize::new(0);
91
92/// Helper function to create a unique DocumentId
93fn new_document_id() -> DocumentId {
94    let namespace_id = new_id_namespace();
95    let id = DOCUMENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
96    DocumentId { namespace_id, id }
97}
98
99/// Direction for cursor navigation
100#[derive(Debug, Copy, Clone, PartialEq, Eq)]
101pub enum CursorNavigationDirection {
102    /// Move cursor up one line
103    Up,
104    /// Move cursor down one line
105    Down,
106    /// Move cursor left one character
107    Left,
108    /// Move cursor right one character
109    Right,
110    /// Move cursor to start of current line
111    LineStart,
112    /// Move cursor to end of current line
113    LineEnd,
114    /// Move cursor to start of document
115    DocumentStart,
116    /// Move cursor to end of document
117    DocumentEnd,
118}
119
120/// Result of a cursor movement operation
121#[derive(Debug, Clone)]
122pub enum CursorMovementResult {
123    /// Cursor moved within the same text node
124    MovedWithinNode(TextCursor),
125    /// Cursor moved to a different text node
126    MovedToNode {
127        dom_id: DomId,
128        node_id: NodeId,
129        cursor: TextCursor,
130    },
131    /// Cursor is at a boundary and cannot move further
132    AtBoundary {
133        boundary: TextBoundary,
134        cursor: TextCursor,
135    },
136}
137
138/// Error when no cursor destination is available
139#[derive(Debug, Clone)]
140pub struct NoCursorDestination {
141    pub reason: String,
142}
143
144/// Action to take for the cursor blink timer when focus changes
145///
146/// This enum is returned by `LayoutWindow::handle_focus_change_for_cursor_blink()`
147/// to tell the platform layer what timer action to take.
148#[derive(Debug, Clone)]
149pub enum CursorBlinkTimerAction {
150    /// Start the cursor blink timer with the given timer configuration
151    Start(crate::timer::Timer),
152    /// Stop the cursor blink timer
153    Stop,
154    /// No change needed (timer already in correct state)
155    NoChange,
156}
157
158/// Helper function to create a unique IdNamespace
159fn new_id_namespace() -> IdNamespace {
160    let id = ID_NAMESPACE_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
161    IdNamespace(id)
162}
163
164// ============================================================================
165// Cursor Blink Timer Callback
166// ============================================================================
167
168/// Destructor for cursor blink timer RefAny (no-op since we use null pointer)
169extern "C" fn cursor_blink_timer_destructor(_: RefAny) {
170    // No cleanup needed - we use a null pointer RefAny
171}
172
173/// Callback for the cursor blink timer
174///
175/// This function is called every ~530ms to toggle cursor visibility.
176/// It checks if enough time has passed since the last user input before blinking,
177/// to avoid blinking while the user is actively typing.
178///
179/// The callback returns:
180/// - `TerminateTimer::Continue` + `Update::RefreshDom` if cursor toggled
181/// - `TerminateTimer::Terminate` if focus is no longer on a contenteditable element
182pub extern "C" fn cursor_blink_timer_callback(
183    _data: RefAny,
184    mut info: crate::timer::TimerCallbackInfo,
185) -> azul_core::callbacks::TimerCallbackReturn {
186    use azul_core::callbacks::{TimerCallbackReturn, Update};
187    use azul_core::task::TerminateTimer;
188    
189    // Get current time
190    let now = info.get_current_time();
191    
192    // We need to access the LayoutWindow through the info
193    // The timer callback needs to:
194    // 1. Check if focus is still on a contenteditable element
195    // 2. Check time since last input
196    // 3. Toggle visibility or keep solid
197    
198    // For now, we'll queue changes via the CallbackInfo system
199    // The actual state modification happens in apply_callback_changes
200    
201    // Check if we should blink or stay solid
202    // This is done by checking CursorManager.should_blink(now) in the layout window
203    
204    // Since we can't access LayoutWindow directly here (it's not passed to timer callbacks),
205    // we use a different approach: the timer callback always toggles, and the visibility
206    // check is done in display_list.rs based on CursorManager state.
207    
208    // Simply toggle cursor visibility
209    info.set_cursor_visibility_toggle();
210    
211    // Continue the timer and request a redraw
212    TimerCallbackReturn {
213        should_update: Update::RefreshDom,
214        should_terminate: TerminateTimer::Continue,
215    }
216}
217
218/// Result of a layout pass for a single DOM, before display list generation
219#[derive(Debug)]
220pub struct DomLayoutResult {
221    /// The styled DOM that was laid out
222    pub styled_dom: StyledDom,
223    /// The layout tree with computed sizes and positions
224    pub layout_tree: LayoutTree,
225    /// Absolute positions of all nodes
226    pub calculated_positions: crate::solver3::PositionVec,
227    /// The viewport used for this layout
228    pub viewport: LogicalRect,
229    /// The generated display list for this DOM.
230    pub display_list: DisplayList,
231    /// Stable scroll IDs computed from node_data_hash
232    /// Maps layout node index -> external scroll ID
233    pub scroll_ids: BTreeMap<usize, u64>,
234    /// Mapping from scroll IDs to DOM NodeIds for hit testing
235    /// This allows us to map WebRender scroll IDs back to DOM nodes
236    pub scroll_id_to_node_id: BTreeMap<u64, NodeId>,
237}
238
239/// State for tracking scrollbar drag interaction
240#[derive(Debug, Clone)]
241pub struct ScrollbarDragState {
242    pub hit_id: ScrollbarHitId,
243    pub initial_mouse_pos: LogicalPosition,
244    pub initial_scroll_offset: LogicalPosition,
245}
246
247/// Information about the last text edit operation
248/// Allows callbacks to query what changed during text input
249// Re-export PendingTextEdit from text_input manager
250pub use crate::managers::text_input::PendingTextEdit;
251
252/// Cached text layout constraints for a node
253/// These are the layout parameters that were used to shape the text
254#[derive(Debug, Clone)]
255pub struct TextConstraintsCache {
256    /// Map from (dom_id, node_id) to their layout constraints
257    pub constraints: BTreeMap<(DomId, NodeId), UnifiedConstraints>,
258}
259
260impl Default for TextConstraintsCache {
261    fn default() -> Self {
262        Self {
263            constraints: BTreeMap::new(),
264        }
265    }
266}
267
268/// A text node that has been edited since the last full layout.
269/// This allows us to perform lightweight relayout without rebuilding the entire DOM.
270#[derive(Debug, Clone)]
271pub struct DirtyTextNode {
272    /// The new inline content (text + images) after editing
273    pub content: Vec<InlineContent>,
274    /// The new cursor position after editing
275    pub cursor: Option<TextCursor>,
276    /// Whether this edit requires ancestor relayout (e.g., text grew taller)
277    pub needs_ancestor_relayout: bool,
278}
279
280/// Result of applying callback changes
281///
282/// This struct consolidates all the outputs from `apply_callback_changes()`,
283/// eliminating the need for 18+ mutable reference parameters.
284#[derive(Debug, Default)]
285pub struct CallbackChangeResult {
286    /// Timers to add
287    pub timers: FastHashMap<TimerId, crate::timer::Timer>,
288    /// Threads to add  
289    pub threads: FastHashMap<ThreadId, crate::thread::Thread>,
290    /// Timers to remove
291    pub timers_removed: FastBTreeSet<TimerId>,
292    /// Threads to remove
293    pub threads_removed: FastBTreeSet<ThreadId>,
294    /// New windows to create
295    pub windows_created: Vec<crate::window_state::WindowCreateOptions>,
296    /// Menus to open
297    pub menus_to_open: Vec<(azul_core::menu::Menu, Option<LogicalPosition>)>,
298    /// Tooltips to show
299    pub tooltips_to_show: Vec<(AzString, LogicalPosition)>,
300    /// Whether to hide tooltip
301    pub hide_tooltip: bool,
302    /// Whether stopPropagation() was called
303    pub stop_propagation: bool,
304    /// Whether stopImmediatePropagation() was called
305    pub stop_immediate_propagation: bool,
306    /// Whether preventDefault() was called
307    pub prevent_default: bool,
308    /// Focus target change
309    pub focus_target: Option<FocusTarget>,
310    /// Text changes that don't require full relayout
311    pub words_changed: BTreeMap<DomId, BTreeMap<NodeId, AzString>>,
312    /// Image changes (for animated images/video)
313    pub images_changed: BTreeMap<DomId, BTreeMap<NodeId, (ImageRef, UpdateImageType)>>,
314    /// Image callback nodes that need to be re-rendered (for resize/animations)
315    /// Unlike images_changed, this triggers a callback re-invocation
316    pub image_callbacks_changed: BTreeMap<DomId, FastBTreeSet<NodeId>>,
317    /// IFrame nodes that need to be re-rendered (for content updates)
318    /// This triggers the IFrame callback to be called with DomRecreated reason
319    pub iframes_to_update: BTreeMap<DomId, FastBTreeSet<NodeId>>,
320    /// Clip mask changes (for vector animations)
321    pub image_masks_changed: BTreeMap<DomId, BTreeMap<NodeId, ImageMask>>,
322    /// CSS property changes from callbacks
323    pub css_properties_changed: BTreeMap<DomId, BTreeMap<NodeId, CssPropertyVec>>,
324    /// Scroll position changes from callbacks
325    pub nodes_scrolled: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, LogicalPosition>>,
326    /// Modified window state
327    pub modified_window_state: FullWindowState,
328    /// Queued window states to apply in sequence (for simulating clicks, etc.)
329    /// Each state will trigger separate event processing to detect state changes.
330    pub queued_window_states: Vec<FullWindowState>,
331    /// Hit test update requested at this position (for Debug API)
332    /// When set, the shell layer should perform a hit test update before processing events
333    pub hit_test_update_requested: Option<LogicalPosition>,
334    /// Text input events triggered by CreateTextInput
335    /// These need to be processed by the recursive event loop to invoke user callbacks
336    pub text_input_triggered: Vec<(azul_core::dom::DomNodeId, Vec<azul_core::events::EventFilter>)>,
337    /// Whether begin_interactive_move() was called (for Wayland xdg_toplevel_move)
338    pub begin_interactive_move: bool,
339}
340
341/// A window-level layout manager that encapsulates all layout state and caching.
342///
343/// This struct owns the layout and text caches, and provides methods dir_to:
344/// - Perform initial layout
345/// - Incrementally update layout on DOM changes
346/// - Generate display lists for rendering
347/// - Handle window resizes efficiently
348/// - Manage multiple DOMs (for IFrames)
349pub struct LayoutWindow {
350    /// Fragmentation context for this window (continuous for screen, paged for print)
351    #[cfg(feature = "pdf")]
352    pub fragmentation_context: crate::paged::FragmentationContext,
353    /// Layout cache for solver3 (incremental layout tree) - for the root DOM
354    pub layout_cache: Solver3LayoutCache,
355    /// Text layout cache for text3 (shaped glyphs, line breaks, etc.)
356    pub text_cache: TextLayoutCache,
357    /// Font manager for loading and caching fonts
358    pub font_manager: FontManager<FontRef>,
359    /// Cache to store decoded images
360    pub image_cache: ImageCache,
361    /// Cached layout results for all DOMs (root + iframes)
362    pub layout_results: BTreeMap<DomId, DomLayoutResult>,
363    /// Scroll state manager for all nodes across all DOMs
364    pub scroll_manager: ScrollManager,
365    /// Gesture and drag manager for multi-frame interactions (moved from FullWindowState)
366    pub gesture_drag_manager: crate::managers::gesture::GestureAndDragManager,
367    /// Focus manager for keyboard focus and tab navigation
368    pub focus_manager: crate::managers::focus_cursor::FocusManager,
369    /// Cursor manager for text cursor position and rendering
370    pub cursor_manager: crate::managers::cursor::CursorManager,
371    /// File drop manager for cursor state and file drag-drop
372    pub file_drop_manager: crate::managers::file_drop::FileDropManager,
373    /// Selection manager for text selections across all DOMs
374    pub selection_manager: crate::managers::selection::SelectionManager,
375    /// Clipboard manager for system clipboard integration
376    pub clipboard_manager: crate::managers::clipboard::ClipboardManager,
377    /// Drag-drop manager for node and file dragging operations
378    pub drag_drop_manager: crate::managers::drag_drop::DragDropManager,
379    /// Hover manager for tracking hit test history over multiple frames
380    pub hover_manager: crate::managers::hover::HoverManager,
381    /// IFrame manager for all nodes across all DOMs
382    pub iframe_manager: IFrameManager,
383    /// GPU state manager for all nodes across all DOMs
384    pub gpu_state_manager: GpuStateManager,
385    /// Accessibility manager for screen reader support
386    pub a11y_manager: crate::managers::a11y::A11yManager,
387    /// Timers associated with this window
388    pub timers: BTreeMap<TimerId, Timer>,
389    /// Threads running in the background for this window
390    pub threads: BTreeMap<ThreadId, Thread>,
391    /// Currently loaded fonts and images present in this renderer (window)
392    pub renderer_resources: RendererResources,
393    /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer?
394    pub renderer_type: Option<RendererType>,
395    /// Windows state of the window of (current frame - 1): initialized to None on startup
396    pub previous_window_state: Option<FullWindowState>,
397    /// Window state of this current window (current frame): initialized to the state of
398    /// WindowCreateOptions
399    pub current_window_state: FullWindowState,
400    /// A "document" in WebRender usually corresponds to one tab (i.e. in Azuls case, the whole
401    /// window).
402    pub document_id: DocumentId,
403    /// ID namespace under which every font / image for this window is registered
404    pub id_namespace: IdNamespace,
405    /// The "epoch" is a frame counter, to remove outdated images, fonts and OpenGL textures when
406    /// they're not in use anymore.
407    pub epoch: Epoch,
408    /// Currently GL textures inside the active CachedDisplayList
409    pub gl_texture_cache: GlTextureCache,
410    /// State for tracking scrollbar drag interaction
411    currently_dragging_thumb: Option<ScrollbarDragState>,
412    /// Text input manager - centralizes all text editing logic
413    pub text_input_manager: crate::managers::text_input::TextInputManager,
414    /// Undo/Redo manager for text editing operations
415    pub undo_redo_manager: crate::managers::undo_redo::UndoRedoManager,
416    /// Cached text layout constraints for each node
417    /// This allows us to re-layout text with the same constraints after edits
418    text_constraints_cache: TextConstraintsCache,
419    /// Tracks which nodes have been edited since last full layout.
420    /// Key: (DomId, NodeId of IFC root)
421    /// Value: The edited inline content that should be used for relayout
422    dirty_text_nodes: BTreeMap<(DomId, NodeId), DirtyTextNode>,
423    /// Pending IFrame updates from callbacks (processed in next frame)
424    /// Map of DomId -> Set of NodeIds that need re-rendering
425    pub pending_iframe_updates: BTreeMap<DomId, FastBTreeSet<NodeId>>,
426    /// System style (colors, fonts, metrics) for resolving system color keywords
427    /// Set via `set_system_style()` from the shell after window creation
428    pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
429    /// Shared monitor list — initialized once at app start, updated by the platform
430    /// layer on monitor topology changes. Arc<Mutex> allows zero-cost sharing
431    /// across all CallbackInfoRefData without cloning the Vec each time.
432    pub monitors: std::sync::Arc<std::sync::Mutex<MonitorVec>>,
433    /// ICU4X localizer handle for internationalized formatting (numbers, dates, lists, plurals)
434    /// Initialized from system language at startup, can be overridden
435    #[cfg(feature = "icu")]
436    pub icu_localizer: IcuLocalizerHandle,
437}
438
439fn default_duration_500ms() -> Duration {
440    Duration::System(SystemTimeDiff::from_millis(500))
441}
442
443fn default_duration_200ms() -> Duration {
444    Duration::System(SystemTimeDiff::from_millis(200))
445}
446
447/// Helper function to convert Duration to milliseconds
448///
449/// Duration is an enum with System (std::time::Duration) and Tick variants.
450/// We need to handle both cases for proper time calculations.
451fn duration_to_millis(duration: Duration) -> u64 {
452    match duration {
453        #[cfg(feature = "std")]
454        Duration::System(system_diff) => {
455            let std_duration: std::time::Duration = system_diff.into();
456            std_duration.as_millis() as u64
457        }
458        #[cfg(not(feature = "std"))]
459        Duration::System(system_diff) => {
460            // Manual calculation: secs * 1000 + nanos / 1_000_000
461            system_diff.secs * 1000 + (system_diff.nanos / 1_000_000) as u64
462        }
463        Duration::Tick(tick_diff) => {
464            // Assume tick = 1ms for simplicity (platform-specific)
465            tick_diff.tick_diff
466        }
467    }
468}
469
470impl LayoutWindow {
471    /// Create a new layout window with empty caches.
472    ///
473    /// For full initialization with WindowInternal compatibility, use `new_full()`.
474    pub fn new(fc_cache: FcFontCache) -> Result<Self, crate::solver3::LayoutError> {
475        Ok(Self {
476            // Default width, will be updated on first layout
477            #[cfg(feature = "pdf")]
478            fragmentation_context: crate::paged::FragmentationContext::new_continuous(800.0),
479            layout_cache: Solver3LayoutCache {
480                tree: None,
481                calculated_positions: Vec::new(),
482                viewport: None,
483                scroll_ids: BTreeMap::new(),
484                scroll_id_to_node_id: BTreeMap::new(),
485                counters: BTreeMap::new(),
486                float_cache: BTreeMap::new(),
487                cache_map: Default::default(),
488            },
489            text_cache: TextLayoutCache::new(),
490            font_manager: FontManager::new(fc_cache)?,
491            image_cache: ImageCache::default(),
492            layout_results: BTreeMap::new(),
493            scroll_manager: ScrollManager::new(),
494            gesture_drag_manager: crate::managers::gesture::GestureAndDragManager::new(),
495            focus_manager: crate::managers::focus_cursor::FocusManager::new(),
496            cursor_manager: crate::managers::cursor::CursorManager::new(),
497            file_drop_manager: crate::managers::file_drop::FileDropManager::new(),
498            selection_manager: crate::managers::selection::SelectionManager::new(),
499            clipboard_manager: crate::managers::clipboard::ClipboardManager::new(),
500            drag_drop_manager: crate::managers::drag_drop::DragDropManager::new(),
501            hover_manager: crate::managers::hover::HoverManager::new(),
502            iframe_manager: IFrameManager::new(),
503            gpu_state_manager: GpuStateManager::new(
504                default_duration_500ms(),
505                default_duration_200ms(),
506            ),
507            a11y_manager: crate::managers::a11y::A11yManager::new(),
508            timers: BTreeMap::new(),
509            threads: BTreeMap::new(),
510            renderer_resources: RendererResources::default(),
511            renderer_type: None,
512            previous_window_state: None,
513            current_window_state: FullWindowState::default(),
514            document_id: new_document_id(),
515            id_namespace: new_id_namespace(),
516            epoch: Epoch::new(),
517            gl_texture_cache: GlTextureCache::default(),
518            currently_dragging_thumb: None,
519            text_input_manager: crate::managers::text_input::TextInputManager::new(),
520            undo_redo_manager: crate::managers::undo_redo::UndoRedoManager::new(),
521            text_constraints_cache: TextConstraintsCache {
522                constraints: BTreeMap::new(),
523            },
524            dirty_text_nodes: BTreeMap::new(),
525            pending_iframe_updates: BTreeMap::new(),
526            system_style: None,
527            monitors: std::sync::Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
528            #[cfg(feature = "icu")]
529            icu_localizer: IcuLocalizerHandle::default(),
530        })
531    }
532
533    /// Create a new layout window for paged media (PDF generation).
534    ///
535    /// This constructor initializes the layout window with a paged fragmentation context,
536    /// which will cause content to flow across multiple pages instead of a single continuous
537    /// scrollable container.
538    ///
539    /// # Arguments
540    /// - `fc_cache`: Font configuration cache for font loading
541    /// - `page_size`: The logical size of each page
542    ///
543    /// # Returns
544    /// A new `LayoutWindow` configured for paged output, or an error if initialization fails.
545    #[cfg(feature = "pdf")]
546    pub fn new_paged(
547        fc_cache: FcFontCache,
548        page_size: LogicalSize,
549    ) -> Result<Self, crate::solver3::LayoutError> {
550        Ok(Self {
551            fragmentation_context: crate::paged::FragmentationContext::new_paged(page_size),
552            layout_cache: Solver3LayoutCache {
553                tree: None,
554                calculated_positions: Vec::new(),
555                viewport: None,
556                scroll_ids: BTreeMap::new(),
557                scroll_id_to_node_id: BTreeMap::new(),
558                counters: BTreeMap::new(),
559                float_cache: BTreeMap::new(),
560                cache_map: Default::default(),
561            },
562            text_cache: TextLayoutCache::new(),
563            font_manager: FontManager::new(fc_cache)?,
564            image_cache: ImageCache::default(),
565            layout_results: BTreeMap::new(),
566            scroll_manager: ScrollManager::new(),
567            gesture_drag_manager: crate::managers::gesture::GestureAndDragManager::new(),
568            focus_manager: crate::managers::focus_cursor::FocusManager::new(),
569            cursor_manager: crate::managers::cursor::CursorManager::new(),
570            file_drop_manager: crate::managers::file_drop::FileDropManager::new(),
571            selection_manager: crate::managers::selection::SelectionManager::new(),
572            clipboard_manager: crate::managers::clipboard::ClipboardManager::new(),
573            drag_drop_manager: crate::managers::drag_drop::DragDropManager::new(),
574            hover_manager: crate::managers::hover::HoverManager::new(),
575            iframe_manager: IFrameManager::new(),
576            gpu_state_manager: GpuStateManager::new(
577                default_duration_500ms(),
578                default_duration_200ms(),
579            ),
580            a11y_manager: crate::managers::a11y::A11yManager::new(),
581            timers: BTreeMap::new(),
582            threads: BTreeMap::new(),
583            renderer_resources: RendererResources::default(),
584            renderer_type: None,
585            previous_window_state: None,
586            current_window_state: FullWindowState::default(),
587            document_id: new_document_id(),
588            id_namespace: new_id_namespace(),
589            epoch: Epoch::new(),
590            gl_texture_cache: GlTextureCache::default(),
591            currently_dragging_thumb: None,
592            text_input_manager: crate::managers::text_input::TextInputManager::new(),
593            undo_redo_manager: crate::managers::undo_redo::UndoRedoManager::new(),
594            text_constraints_cache: TextConstraintsCache {
595                constraints: BTreeMap::new(),
596            },
597            dirty_text_nodes: BTreeMap::new(),
598            pending_iframe_updates: BTreeMap::new(),
599            system_style: None,
600            monitors: std::sync::Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
601            #[cfg(feature = "icu")]
602            icu_localizer: IcuLocalizerHandle::default(),
603        })
604    }
605
606    /// Perform layout on a styled DOM and generate a display list.
607    ///
608    /// This is the main entry point for layout. It handles:
609    /// - Incremental layout updates using the cached layout tree
610    /// - Text shaping and line breaking
611    /// - IFrame callback invocation and recursive layout
612    /// - Display list generation for rendering
613    /// - Accessibility tree synchronization
614    ///
615    /// # Arguments
616    /// - `styled_dom`: The styled DOM to layout
617    /// - `window_state`: Current window dimensions and state
618    /// - `renderer_resources`: Resources for image sizing etc.
619    /// - `debug_messages`: Optional vector to collect debug/warning messages
620    ///
621    /// # Returns
622    /// The display list ready for rendering, or an error if layout fails.
623    pub fn layout_and_generate_display_list(
624        &mut self,
625        root_dom: StyledDom,
626        window_state: &FullWindowState,
627        renderer_resources: &RendererResources,
628        system_callbacks: &ExternalSystemCallbacks,
629        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
630    ) -> Result<(), solver3::LayoutError> {
631        // Clear previous results for a full relayout
632        self.layout_results.clear();
633
634        if let Some(msgs) = debug_messages.as_mut() {
635            msgs.push(LayoutDebugMessage::info(format!(
636                "[layout_and_generate_display_list] Starting layout for DOM with {} nodes",
637                root_dom.node_data.len()
638            )));
639        }
640
641        // Start recursive layout from the root DOM
642        let result = self.layout_dom_recursive(
643            root_dom,
644            window_state,
645            renderer_resources,
646            system_callbacks,
647            debug_messages,
648        );
649
650        if let Err(ref e) = result {
651            if let Some(msgs) = debug_messages.as_mut() {
652                msgs.push(LayoutDebugMessage::error(format!(
653                    "[layout_and_generate_display_list] Layout FAILED: {:?}",
654                    e
655                )));
656            }
657        } else {
658            if let Some(msgs) = debug_messages.as_mut() {
659                msgs.push(LayoutDebugMessage::info(format!(
660                    "[layout_and_generate_display_list] Layout SUCCESS, layout_results count: {}",
661                    self.layout_results.len()
662                )));
663            }
664        }
665
666        // After successful layout, update the accessibility tree
667        // Note: This is wrapped in catch_unwind to prevent a11y issues from crashing the app
668        #[cfg(feature = "a11y")]
669        if result.is_ok() {
670            // Use catch_unwind to prevent a11y panics from crashing the main application
671            let a11y_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
672                crate::managers::a11y::A11yManager::update_tree(
673                    self.a11y_manager.root_id,
674                    &self.layout_results,
675                    &self.current_window_state.title,
676                    self.current_window_state.size.dimensions,
677                )
678            }));
679
680            match a11y_result {
681                Ok(tree_update) => {
682                    // Store the tree_update for platform adapter to consume
683                    self.a11y_manager.last_tree_update = Some(tree_update);
684                }
685                Err(_) => {
686                    // A11y update failed - log and continue without a11y
687                }
688            }
689        }
690
691        // After layout, automatically scroll cursor into view if there's a focused text input
692        if result.is_ok() {
693            self.scroll_focused_cursor_into_view();
694        }
695
696        result
697    }
698
699    fn layout_dom_recursive(
700        &mut self,
701        mut styled_dom: StyledDom,
702        window_state: &FullWindowState,
703        renderer_resources: &RendererResources,
704        system_callbacks: &ExternalSystemCallbacks,
705        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
706    ) -> Result<(), solver3::LayoutError> {
707        if styled_dom.dom_id.inner == 0 {
708            styled_dom.dom_id = DomId::ROOT_ID;
709        }
710        let dom_id = styled_dom.dom_id;
711
712        let viewport = LogicalRect {
713            origin: LogicalPosition::zero(),
714            size: window_state.size.dimensions,
715        };
716
717        // Get the platform from system_style, falling back to compile-time detection
718        let platform = self.system_style.as_ref()
719            .map(|s| s.platform.clone())
720            .unwrap_or_else(azul_css::system::Platform::current);
721
722        // Font Resolution And Loading
723        // This must happen BEFORE layout_document() is called
724        {
725            use crate::{
726                solver3::getters::{
727                    collect_and_resolve_font_chains, collect_font_ids_from_chains,
728                    compute_fonts_to_load, load_fonts_from_disk, register_embedded_fonts_from_styled_dom,
729                },
730                text3::default::PathLoader,
731            };
732
733            if let Some(msgs) = debug_messages.as_mut() {
734                msgs.push(LayoutDebugMessage::info(
735                    "[FontLoading] Starting font resolution for DOM".to_string(),
736                ));
737            }
738
739            // Step 0: Register embedded FontRefs (e.g. Material Icons)
740            // These fonts bypass fontconfig and are used directly
741            register_embedded_fonts_from_styled_dom(&styled_dom, &self.font_manager, &platform);
742
743            // Step 1: Resolve font chains (cached by FontChainKey)
744            let chains = collect_and_resolve_font_chains(&styled_dom, &self.font_manager.fc_cache, &platform);
745            if let Some(msgs) = debug_messages.as_mut() {
746                msgs.push(LayoutDebugMessage::info(format!(
747                    "[FontLoading] Resolved {} font chains",
748                    chains.len()
749                )));
750            }
751
752            // Step 2: Get required font IDs from chains
753            let required_fonts = collect_font_ids_from_chains(&chains);
754            if let Some(msgs) = debug_messages.as_mut() {
755                msgs.push(LayoutDebugMessage::info(format!(
756                    "[FontLoading] Required fonts: {} unique fonts",
757                    required_fonts.len()
758                )));
759            }
760
761            // Step 3: Compute which fonts need to be loaded (diff with already loaded)
762            let already_loaded = self.font_manager.get_loaded_font_ids();
763            let fonts_to_load = compute_fonts_to_load(&required_fonts, &already_loaded);
764            if let Some(msgs) = debug_messages.as_mut() {
765                msgs.push(LayoutDebugMessage::info(format!(
766                    "[FontLoading] Already loaded: {}, need to load: {}",
767                    already_loaded.len(),
768                    fonts_to_load.len()
769                )));
770            }
771
772            // Step 4: Load missing fonts
773            if !fonts_to_load.is_empty() {
774                if let Some(msgs) = debug_messages.as_mut() {
775                    msgs.push(LayoutDebugMessage::info(format!(
776                        "[FontLoading] Loading {} fonts from disk...",
777                        fonts_to_load.len()
778                    )));
779                }
780                let loader = PathLoader::new();
781                let load_result = load_fonts_from_disk(
782                    &fonts_to_load,
783                    &self.font_manager.fc_cache,
784                    |bytes, index| loader.load_font(bytes, index),
785                );
786
787                if let Some(msgs) = debug_messages.as_mut() {
788                    msgs.push(LayoutDebugMessage::info(format!(
789                        "[FontLoading] Loaded {} fonts, {} failed",
790                        load_result.loaded.len(),
791                        load_result.failed.len()
792                    )));
793                }
794
795                // Insert loaded fonts into the font manager
796                self.font_manager.insert_fonts(load_result.loaded);
797
798                // Log any failures
799                for (font_id, error) in &load_result.failed {
800                    if let Some(msgs) = debug_messages.as_mut() {
801                        msgs.push(LayoutDebugMessage::warning(format!(
802                            "[FontLoading] Failed to load font {:?}: {}",
803                            font_id, error
804                        )));
805                    }
806                }
807            }
808
809            // Step 5: Update font chain cache
810            self.font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
811        }
812
813        let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
814        let styled_dom_clone = styled_dom.clone();
815        let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id).clone();
816        
817        // Get cursor visibility from cursor manager for display list generation
818        let cursor_is_visible = self.cursor_manager.should_draw_cursor();
819        
820        // Get cursor location from cursor manager for independent cursor rendering
821        let cursor_location = self.cursor_manager.get_cursor_location().and_then(|loc| {
822            self.cursor_manager.get_cursor().map(|cursor| {
823                (loc.dom_id, loc.node_id, cursor.clone())
824            })
825        });
826
827        let mut display_list = solver3::layout_document(
828            &mut self.layout_cache,
829            &mut self.text_cache,
830            styled_dom,
831            viewport,
832            &self.font_manager,
833            &scroll_offsets,
834            &self.selection_manager.selections,
835            &self.selection_manager.text_selections,
836            debug_messages,
837            Some(&gpu_cache),
838            &self.renderer_resources,
839            self.id_namespace,
840            dom_id,
841            cursor_is_visible,
842            cursor_location,
843            self.system_style.clone(),
844            system_callbacks.get_system_time_fn,
845        )?;
846
847        let tree = self
848            .layout_cache
849            .tree
850            .clone()
851            .ok_or(solver3::LayoutError::InvalidTree)?;
852
853        // Get scroll IDs from cache (they were computed during layout_document)
854        let scroll_ids = self.layout_cache.scroll_ids.clone();
855        let scroll_id_to_node_id = self.layout_cache.scroll_id_to_node_id.clone();
856
857        // Synchronize scrollbar transforms AFTER layout
858        self.gpu_state_manager
859            .update_scrollbar_transforms(dom_id, &self.scroll_manager, &tree);
860
861        // Scan for IFrames *after* the initial layout pass
862        // Pass styled_dom directly — layout_results isn't populated yet at this point
863        let iframes = self.scan_for_iframes(&styled_dom_clone, &tree, &self.layout_cache.calculated_positions);
864
865        for (node_id, bounds) in iframes {
866            if let Some(child_dom_id) = self.invoke_iframe_callback_with_dom(
867                dom_id,
868                node_id,
869                bounds,
870                Some(&styled_dom_clone),
871                window_state,
872                renderer_resources,
873                system_callbacks,
874                debug_messages,
875            ) {
876                // Insert an IFrame primitive that the renderer will use
877                display_list
878                    .items
879                    .push(crate::solver3::display_list::DisplayListItem::IFrame {
880                        child_dom_id,
881                        bounds,
882                        clip_rect: bounds,
883                    });
884            }
885        }
886
887        // Store the final layout result for this DOM
888        self.layout_results.insert(
889            dom_id,
890            DomLayoutResult {
891                styled_dom: styled_dom_clone,
892                layout_tree: tree,
893                calculated_positions: self.layout_cache.calculated_positions.clone(),
894                viewport,
895                display_list,
896                scroll_ids,
897                scroll_id_to_node_id,
898            },
899        );
900
901        Ok(())
902    }
903
904    fn scan_for_iframes(
905        &self,
906        styled_dom: &StyledDom,
907        layout_tree: &LayoutTree,
908        calculated_positions: &crate::solver3::PositionVec,
909    ) -> Vec<(NodeId, LogicalRect)> {
910        let node_data_container = styled_dom.node_data.as_container();
911        layout_tree
912            .nodes
913            .iter()
914            .enumerate()
915            .filter_map(|(idx, node)| {
916                let node_dom_id = node.dom_node_id?;
917                let node_data = node_data_container.get(node_dom_id)?;
918                if matches!(node_data.get_node_type(), NodeType::IFrame(_)) {
919                    let pos = calculated_positions.get(idx).copied().unwrap_or_default();
920                    let size = node.used_size.unwrap_or_default();
921                    Some((node_dom_id, LogicalRect::new(pos, size)))
922                } else {
923                    None
924                }
925            })
926            .collect()
927    }
928
929    /// Handle a window resize by updating the cached layout.
930    ///
931    /// This method leverages solver3's incremental layout system to efficiently
932    /// relayout only the affected parts of the tree when the window size changes.
933    ///
934    /// Returns the new display list after the resize.
935    pub fn resize_window(
936        &mut self,
937        styled_dom: StyledDom,
938        new_size: LogicalSize,
939        renderer_resources: &RendererResources,
940        system_callbacks: &ExternalSystemCallbacks,
941        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
942    ) -> Result<DisplayList, crate::solver3::LayoutError> {
943        // Create a temporary FullWindowState with the new size
944        let mut window_state = FullWindowState::default();
945        window_state.size.dimensions = new_size;
946
947        let dom_id = styled_dom.dom_id;
948
949        // Reuse the main layout method - solver3 will detect the viewport
950        // change and invalidate only what's necessary
951        self.layout_and_generate_display_list(
952            styled_dom,
953            &window_state,
954            renderer_resources,
955            system_callbacks,
956            debug_messages,
957        )?;
958
959        // Retrieve the display list from the layout result
960        // We need to take ownership of the display list, so we replace it with an empty one
961        self.layout_results
962            .get_mut(&dom_id)
963            .map(|result| std::mem::replace(&mut result.display_list, DisplayList::default()))
964            .ok_or(solver3::LayoutError::InvalidTree)
965    }
966
967    /// Clear all caches (useful for testing or when switching documents).
968    pub fn clear_caches(&mut self) {
969        self.layout_cache = Solver3LayoutCache {
970            tree: None,
971            calculated_positions: Vec::new(),
972            viewport: None,
973            scroll_ids: BTreeMap::new(),
974            scroll_id_to_node_id: BTreeMap::new(),
975            counters: BTreeMap::new(),
976            float_cache: BTreeMap::new(),
977            cache_map: Default::default(),
978        };
979        self.text_cache = TextLayoutCache::new();
980        self.layout_results.clear();
981        self.scroll_manager = ScrollManager::new();
982        self.selection_manager.clear_all();
983    }
984
985    /// Set scroll position for a node
986    pub fn set_scroll_position(&mut self, dom_id: DomId, node_id: NodeId, scroll: ScrollPosition) {
987        // Convert ScrollPosition to the internal representation
988        #[cfg(feature = "std")]
989        let now = Instant::System(std::time::Instant::now().into());
990        #[cfg(not(feature = "std"))]
991        let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
992
993        self.scroll_manager.update_node_bounds(
994            dom_id,
995            node_id,
996            scroll.parent_rect,
997            scroll.children_rect,
998            now.clone(),
999        );
1000        self.scroll_manager
1001            .set_scroll_position(dom_id, node_id, scroll.children_rect.origin, now);
1002    }
1003
1004    /// Get scroll position for a node
1005    pub fn get_scroll_position(&self, dom_id: DomId, node_id: NodeId) -> Option<ScrollPosition> {
1006        let states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
1007        states.get(&node_id).cloned()
1008    }
1009
1010    /// Set selection state for a DOM
1011    pub fn set_selection(&mut self, dom_id: DomId, selection: SelectionState) {
1012        self.selection_manager.set_selection(dom_id, selection);
1013    }
1014
1015    /// Get selection state for a DOM
1016    pub fn get_selection(&self, dom_id: DomId) -> Option<&SelectionState> {
1017        self.selection_manager.get_selection(&dom_id)
1018    }
1019
1020    /// Invoke an IFrame callback and perform layout on the returned DOM.
1021    ///
1022    /// This is the entry point that looks up the necessary `IFrameNode` data before
1023    /// delegating to the core implementation logic.
1024    /// Invoke an IFrame callback for a node. Returns the child DomId if the
1025    /// callback was invoked and the child DOM was laid out.
1026    ///
1027    /// This calls the IFrame's own RefAny callback (NOT the main layout() callback),
1028    /// swaps the child StyledDom, and re-layouts only the IFrame sub-tree.
1029    pub fn invoke_iframe_callback(
1030        &mut self,
1031        parent_dom_id: DomId,
1032        node_id: NodeId,
1033        bounds: LogicalRect,
1034        window_state: &FullWindowState,
1035        renderer_resources: &RendererResources,
1036        system_callbacks: &ExternalSystemCallbacks,
1037        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1038    ) -> Option<DomId> {
1039        self.invoke_iframe_callback_with_dom(
1040            parent_dom_id, node_id, bounds, None,
1041            window_state, renderer_resources, system_callbacks, debug_messages,
1042        )
1043    }
1044
1045    /// Invoke an IFrame callback. If `styled_dom_override` is provided, use it
1046    /// instead of reading from `self.layout_results` (needed during initial
1047    /// layout when layout_results isn't populated yet).
1048    fn invoke_iframe_callback_with_dom(
1049        &mut self,
1050        parent_dom_id: DomId,
1051        node_id: NodeId,
1052        bounds: LogicalRect,
1053        styled_dom_override: Option<&StyledDom>,
1054        window_state: &FullWindowState,
1055        renderer_resources: &RendererResources,
1056        system_callbacks: &ExternalSystemCallbacks,
1057        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1058    ) -> Option<DomId> {
1059        if let Some(msgs) = debug_messages {
1060            msgs.push(LayoutDebugMessage::info(format!(
1061                "invoke_iframe_callback called for node {:?}",
1062                node_id
1063            )));
1064        }
1065
1066        // Use the override styled_dom if provided, otherwise read from layout_results
1067        let iframe_node = if let Some(styled_dom) = styled_dom_override {
1068            let node_data_container = styled_dom.node_data.as_container();
1069            let node_data = node_data_container.get(node_id)?;
1070            match node_data.get_node_type() {
1071                NodeType::IFrame(iframe) => iframe.clone(),
1072                _ => return None,
1073            }
1074        } else {
1075            let layout_result = self.layout_results.get(&parent_dom_id)?;
1076            if let Some(msgs) = debug_messages {
1077                msgs.push(LayoutDebugMessage::info(format!(
1078                    "Got layout result for parent DOM {:?}",
1079                    parent_dom_id
1080                )));
1081            }
1082            let node_data_container = layout_result.styled_dom.node_data.as_container();
1083            let node_data = node_data_container.get(node_id)?;
1084            match node_data.get_node_type() {
1085                NodeType::IFrame(iframe) => iframe.clone(),
1086                other => {
1087                    if let Some(msgs) = debug_messages {
1088                        msgs.push(LayoutDebugMessage::info(format!(
1089                            "Node is NOT IFrame, type = {:?}",
1090                            other
1091                        )));
1092                    }
1093                    return None;
1094                }
1095            }
1096        };
1097
1098        if let Some(msgs) = debug_messages {
1099            msgs.push(LayoutDebugMessage::info("Node is IFrame type".to_string()));
1100        }
1101
1102        // Call the actual implementation with all necessary data
1103        self.invoke_iframe_callback_impl(
1104            parent_dom_id,
1105            node_id,
1106            &iframe_node,
1107            bounds,
1108            window_state,
1109            renderer_resources,
1110            system_callbacks,
1111            debug_messages,
1112        )
1113    }
1114
1115    /// Core implementation for invoking an IFrame callback and managing the recursive layout.
1116    ///
1117    /// This method implements the 5 conditional re-invocation rules by coordinating
1118    /// with the `IFrameManager` and `ScrollManager`.
1119    ///
1120    /// # Returns
1121    ///
1122    /// `Some(child_dom_id)` if the callback was invoked and the child DOM was laid out.
1123    /// The parent's display list generator will then use this ID to reference the child's
1124    /// display list. Returns `None` if the callback was not invoked.
1125    fn invoke_iframe_callback_impl(
1126        &mut self,
1127        parent_dom_id: DomId,
1128        node_id: NodeId,
1129        iframe_node: &azul_core::dom::IFrameNode,
1130        bounds: LogicalRect,
1131        window_state: &FullWindowState,
1132        renderer_resources: &RendererResources,
1133        system_callbacks: &ExternalSystemCallbacks,
1134        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1135    ) -> Option<DomId> {
1136        // Get current time from system callbacks for state updates
1137        let now = (system_callbacks.get_system_time_fn.cb)();
1138
1139        // Update node bounds in the scroll manager. This is necessary for the IFrameManager
1140        // to correctly detect edge scroll conditions.
1141        self.scroll_manager.update_node_bounds(
1142            parent_dom_id,
1143            node_id,
1144            bounds,
1145            LogicalRect::new(LogicalPosition::zero(), bounds.size), // Initial content_rect
1146            now.clone(),
1147        );
1148
1149        // Check with the IFrameManager to see if re-invocation is necessary.
1150        // It handles all 5 conditional rules.
1151        let reason = match self.iframe_manager.check_reinvoke(
1152            parent_dom_id,
1153            node_id,
1154            &self.scroll_manager,
1155            bounds,
1156        ) {
1157            Some(r) => r,
1158            None => {
1159                // No re-invocation needed, but we still need the child_dom_id for the display list.
1160                return self
1161                    .iframe_manager
1162                    .get_nested_dom_id(parent_dom_id, node_id);
1163            }
1164        };
1165
1166        if let Some(msgs) = debug_messages {
1167            msgs.push(LayoutDebugMessage::info(format!(
1168                "IFrame ({:?}, {:?}) - Reason: {:?}",
1169                parent_dom_id, node_id, reason
1170            )));
1171        }
1172
1173        let scroll_offset = self
1174            .scroll_manager
1175            .get_current_offset(parent_dom_id, node_id)
1176            .unwrap_or_default();
1177        let hidpi_factor = window_state.size.get_hidpi_factor();
1178
1179        // Create IFrameCallbackInfo with the most up-to-date state
1180        let mut callback_info = azul_core::callbacks::IFrameCallbackInfo::new(
1181            reason,
1182            &*self.font_manager.fc_cache,
1183            &self.image_cache,
1184            window_state.theme,
1185            azul_core::callbacks::HidpiAdjustedBounds {
1186                logical_size: bounds.size,
1187                hidpi_factor,
1188            },
1189            bounds.size,
1190            scroll_offset,
1191            bounds.size,
1192            LogicalPosition::zero(),
1193        );
1194
1195        // Clone the user data for the callback
1196        let callback_data = iframe_node.refany.clone();
1197
1198        // Invoke the user's IFrame callback
1199        let callback_return = (iframe_node.callback.cb)(callback_data, callback_info);
1200
1201        // Mark the IFrame as invoked to prevent duplicate InitialRender calls
1202        self.iframe_manager
1203            .mark_invoked(parent_dom_id, node_id, reason);
1204
1205        // Get the child StyledDom from the callback's return value
1206        let mut child_styled_dom = match callback_return.dom {
1207            azul_core::styled_dom::OptionStyledDom::Some(dom) => dom,
1208            azul_core::styled_dom::OptionStyledDom::None => {
1209                // If the callback returns None, it's an optimization hint.
1210                if reason == IFrameCallbackReason::InitialRender {
1211                    // For the very first render, create an empty div as a fallback.
1212                    let mut empty_dom = Dom::create_div();
1213                    let empty_css = Css::empty();
1214                    empty_dom.style(empty_css)
1215                } else {
1216                    // For subsequent calls, returning None means "keep the old DOM".
1217                    // We just need to update the scroll info and return the existing child ID.
1218                    self.iframe_manager.update_iframe_info(
1219                        parent_dom_id,
1220                        node_id,
1221                        callback_return.scroll_size,
1222                        callback_return.virtual_scroll_size,
1223                    );
1224                    // Propagate virtual scroll bounds to ScrollManager
1225                    self.scroll_manager.update_virtual_scroll_bounds(
1226                        parent_dom_id,
1227                        node_id,
1228                        callback_return.virtual_scroll_size,
1229                        Some(callback_return.scroll_offset),
1230                    );
1231                    return self
1232                        .iframe_manager
1233                        .get_nested_dom_id(parent_dom_id, node_id);
1234                }
1235            }
1236        };
1237
1238        // Get or create a unique DomId for the IFrame's content
1239        let child_dom_id = self
1240            .iframe_manager
1241            .get_or_create_nested_dom_id(parent_dom_id, node_id);
1242        child_styled_dom.dom_id = child_dom_id;
1243
1244        // Update the IFrameManager with the new scroll sizes from the callback
1245        self.iframe_manager.update_iframe_info(
1246            parent_dom_id,
1247            node_id,
1248            callback_return.scroll_size,
1249            callback_return.virtual_scroll_size,
1250        );
1251        // Propagate virtual scroll bounds to ScrollManager
1252        self.scroll_manager.update_virtual_scroll_bounds(
1253            parent_dom_id,
1254            node_id,
1255            callback_return.virtual_scroll_size,
1256            Some(callback_return.scroll_offset),
1257        );
1258
1259        // **RECURSIVE LAYOUT STEP**
1260        // Perform a full layout pass on the child DOM. This will recursively handle
1261        // any IFrames within this IFrame.
1262        self.layout_dom_recursive(
1263            child_styled_dom,
1264            window_state,
1265            renderer_resources,
1266            system_callbacks,
1267            debug_messages,
1268        )
1269        .ok()?;
1270
1271        Some(child_dom_id)
1272    }
1273
1274    // Query methods for callbacks
1275
1276    /// Get the size of a laid-out node
1277    pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1278        let layout_result = self.layout_results.get(&node_id.dom)?;
1279        let nid = node_id.node.into_crate_internal()?;
1280        // Use dom_to_layout mapping since layout tree indices differ from DOM indices
1281        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
1282        let layout_index = *layout_indices.first()?;
1283        let layout_node = layout_result.layout_tree.get(layout_index)?;
1284        layout_node.used_size
1285    }
1286
1287    /// Get the position of a laid-out node
1288    pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1289        let layout_result = self.layout_results.get(&node_id.dom)?;
1290        let nid = node_id.node.into_crate_internal()?;
1291        // Use dom_to_layout mapping since layout tree indices differ from DOM indices
1292        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
1293        let layout_index = *layout_indices.first()?;
1294        let position = layout_result.calculated_positions.get(layout_index)?;
1295        Some(*position)
1296    }
1297
1298    /// Get the hit test bounds of a node from the display list
1299    ///
1300    /// This is more reliable than get_node_position + get_node_size because
1301    /// the display list always contains the correct final rendered positions,
1302    /// including for nodes that may not have entries in calculated_positions.
1303    pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1304        use crate::solver3::display_list::DisplayListItem;
1305
1306        let layout_result = self.layout_results.get(&node_id.dom)?;
1307        let nid = node_id.node.into_crate_internal()?;
1308
1309        // Look up tag_id from the authoritative tag_ids_to_node_ids mapping
1310        let nid_encoded = NodeHierarchyItemId::from_crate_internal(Some(nid));
1311        let tag_id = layout_result.styled_dom.tag_ids_to_node_ids.iter()
1312            .find(|m| m.node_id == nid_encoded)?
1313            .tag_id
1314            .inner;
1315
1316        // Search the display list for a HitTestArea with matching tag
1317        // Note: tag is now (u64, u16) tuple where tag.0 is the TagId.inner
1318        for item in &layout_result.display_list.items {
1319            if let DisplayListItem::HitTestArea { bounds, tag } = item {
1320                if tag.0 == tag_id && bounds.size.width > 0.0 && bounds.size.height > 0.0 {
1321                    return Some(*bounds);
1322                }
1323            }
1324        }
1325        None
1326    }
1327
1328    /// Get the parent of a node
1329    pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1330        let layout_result = self.layout_results.get(&node_id.dom)?;
1331        let nid = node_id.node.into_crate_internal()?;
1332        let parent_id = layout_result
1333            .styled_dom
1334            .node_hierarchy
1335            .as_container()
1336            .get(nid)?
1337            .parent_id()?;
1338        Some(DomNodeId {
1339            dom: node_id.dom,
1340            node: NodeHierarchyItemId::from_crate_internal(Some(parent_id)),
1341        })
1342    }
1343
1344    /// Get the first child of a node
1345    pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1346        let layout_result = self.layout_results.get(&node_id.dom)?;
1347        let nid = node_id.node.into_crate_internal()?;
1348        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1349        let hierarchy_item = node_hierarchy.get(nid)?;
1350        let first_child_id = hierarchy_item.first_child_id(nid)?;
1351        Some(DomNodeId {
1352            dom: node_id.dom,
1353            node: NodeHierarchyItemId::from_crate_internal(Some(first_child_id)),
1354        })
1355    }
1356
1357    /// Get the next sibling of a node
1358    pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1359        let layout_result = self.layout_results.get(&node_id.dom)?;
1360        let nid = node_id.node.into_crate_internal()?;
1361        let next_sibling_id = layout_result
1362            .styled_dom
1363            .node_hierarchy
1364            .as_container()
1365            .get(nid)?
1366            .next_sibling_id()?;
1367        Some(DomNodeId {
1368            dom: node_id.dom,
1369            node: NodeHierarchyItemId::from_crate_internal(Some(next_sibling_id)),
1370        })
1371    }
1372
1373    /// Get the previous sibling of a node
1374    pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1375        let layout_result = self.layout_results.get(&node_id.dom)?;
1376        let nid = node_id.node.into_crate_internal()?;
1377        let prev_sibling_id = layout_result
1378            .styled_dom
1379            .node_hierarchy
1380            .as_container()
1381            .get(nid)?
1382            .previous_sibling_id()?;
1383        Some(DomNodeId {
1384            dom: node_id.dom,
1385            node: NodeHierarchyItemId::from_crate_internal(Some(prev_sibling_id)),
1386        })
1387    }
1388
1389    /// Get the last child of a node
1390    pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1391        let layout_result = self.layout_results.get(&node_id.dom)?;
1392        let nid = node_id.node.into_crate_internal()?;
1393        let last_child_id = layout_result
1394            .styled_dom
1395            .node_hierarchy
1396            .as_container()
1397            .get(nid)?
1398            .last_child_id()?;
1399        Some(DomNodeId {
1400            dom: node_id.dom,
1401            node: NodeHierarchyItemId::from_crate_internal(Some(last_child_id)),
1402        })
1403    }
1404
1405    /// Scan all fonts used in this LayoutWindow (for resource GC)
1406    pub fn scan_used_fonts(&self) -> BTreeSet<FontKey> {
1407        let mut fonts = BTreeSet::new();
1408        for (_dom_id, layout_result) in &self.layout_results {
1409            // TODO: Scan styled_dom for font references
1410            // This requires accessing the CSS property cache and finding all font-family properties
1411        }
1412        fonts
1413    }
1414
1415    /// Scan all images used in this LayoutWindow (for resource GC)
1416    pub fn scan_used_images(&self, _css_image_cache: &ImageCache) -> BTreeSet<ImageRefHash> {
1417        let mut images = BTreeSet::new();
1418        for (_dom_id, layout_result) in &self.layout_results {
1419            // TODO: Scan styled_dom for image references
1420            // This requires scanning background-image and content properties
1421        }
1422        images
1423    }
1424
1425    /// Helper function to convert ScrollStates to nested format for CallbackInfo
1426    fn get_nested_scroll_states(
1427        &self,
1428        dom_id: DomId,
1429    ) -> BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> {
1430        let mut nested = BTreeMap::new();
1431        let scroll_states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
1432        let mut inner = BTreeMap::new();
1433        for (node_id, scroll_pos) in scroll_states {
1434            inner.insert(
1435                NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1436                scroll_pos,
1437            );
1438        }
1439        nested.insert(dom_id, inner);
1440        nested
1441    }
1442
1443    // Scroll Into View
1444    
1445    /// Scroll a DOM node into view
1446    ///
1447    /// This is the main API for scrolling elements into view. It handles:
1448    /// - Finding scroll ancestors
1449    /// - Calculating scroll deltas
1450    /// - Applying scroll animations
1451    ///
1452    /// # Arguments
1453    ///
1454    /// * `node_id` - The DOM node to scroll into view
1455    /// * `options` - Scroll alignment and animation options
1456    /// * `now` - Current timestamp for animations
1457    ///
1458    /// # Returns
1459    ///
1460    /// A vector of scroll adjustments that were applied
1461    pub fn scroll_node_into_view(
1462        &mut self,
1463        node_id: DomNodeId,
1464        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1465        now: azul_core::task::Instant,
1466    ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
1467        crate::managers::scroll_into_view::scroll_node_into_view(
1468            node_id,
1469            &self.layout_results,
1470            &mut self.scroll_manager,
1471            options,
1472            now,
1473        )
1474    }
1475    
1476    /// Scroll a text cursor into view
1477    ///
1478    /// Used when the cursor moves within a contenteditable element.
1479    /// The cursor rect should be in node-local coordinates.
1480    pub fn scroll_cursor_into_view(
1481        &mut self,
1482        cursor_rect: LogicalRect,
1483        node_id: DomNodeId,
1484        options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1485        now: azul_core::task::Instant,
1486    ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
1487        crate::managers::scroll_into_view::scroll_cursor_into_view(
1488            cursor_rect,
1489            node_id,
1490            &self.layout_results,
1491            &mut self.scroll_manager,
1492            options,
1493            now,
1494        )
1495    }
1496
1497    // Timer Management
1498
1499    /// Add a timer to this window
1500    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
1501        self.timers.insert(timer_id, timer);
1502    }
1503
1504    /// Remove a timer from this window
1505    pub fn remove_timer(&mut self, timer_id: &TimerId) -> Option<Timer> {
1506        self.timers.remove(timer_id)
1507    }
1508
1509    /// Get a reference to a timer
1510    pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1511        self.timers.get(timer_id)
1512    }
1513
1514    /// Get a mutable reference to a timer
1515    pub fn get_timer_mut(&mut self, timer_id: &TimerId) -> Option<&mut Timer> {
1516        self.timers.get_mut(timer_id)
1517    }
1518
1519    /// Get all timer IDs
1520    pub fn get_timer_ids(&self) -> TimerIdVec {
1521        self.timers.keys().copied().collect::<Vec<_>>().into()
1522    }
1523
1524    /// Tick all timers (called once per frame)
1525    /// Returns a list of timer IDs that are ready to run
1526    pub fn tick_timers(&mut self, current_time: azul_core::task::Instant) -> Vec<TimerId> {
1527        let mut ready_timers = Vec::new();
1528
1529        for (timer_id, timer) in &mut self.timers {
1530            // Check if timer is ready to run
1531            // This logic should match the timer's internal state
1532            // For now, we'll just collect all timer IDs
1533            // The actual readiness check will be done when invoking
1534            ready_timers.push(*timer_id);
1535        }
1536
1537        ready_timers
1538    }
1539
1540    /// Calculate milliseconds until the next timer needs to fire.
1541    ///
1542    /// Returns `None` if there are no timers, meaning the caller can block indefinitely.
1543    /// Returns `Some(0)` if a timer is already overdue.
1544    /// Otherwise returns the minimum time in milliseconds until any timer fires.
1545    ///
1546    /// This is used by Linux (X11/Wayland) to set an efficient poll/select timeout
1547    /// instead of always polling every 16ms.
1548    pub fn time_until_next_timer_ms(
1549        &self,
1550        get_system_time_fn: &azul_core::task::GetSystemTimeCallback,
1551    ) -> Option<u64> {
1552        if self.timers.is_empty() {
1553            return None; // No timers - can block indefinitely
1554        }
1555
1556        let now = (get_system_time_fn.cb)();
1557        let mut min_ms: Option<u64> = None;
1558
1559        for timer in self.timers.values() {
1560            let next_run = timer.instant_of_next_run();
1561
1562            // Calculate time difference in milliseconds
1563            let ms_until = if next_run < now {
1564                0 // Timer is overdue
1565            } else {
1566                duration_to_millis(next_run.duration_since(&now))
1567            };
1568
1569            min_ms = Some(match min_ms {
1570                Some(current_min) => current_min.min(ms_until),
1571                None => ms_until,
1572            });
1573        }
1574
1575        min_ms
1576    }
1577
1578    // Thread Management
1579
1580    /// Add a thread to this window
1581    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
1582        self.threads.insert(thread_id, thread);
1583    }
1584
1585    /// Remove a thread from this window
1586    pub fn remove_thread(&mut self, thread_id: &ThreadId) -> Option<Thread> {
1587        self.threads.remove(thread_id)
1588    }
1589
1590    /// Get a reference to a thread
1591    pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1592        self.threads.get(thread_id)
1593    }
1594
1595    /// Get a mutable reference to a thread
1596    pub fn get_thread_mut(&mut self, thread_id: &ThreadId) -> Option<&mut Thread> {
1597        self.threads.get_mut(thread_id)
1598    }
1599
1600    /// Get all thread IDs
1601    pub fn get_thread_ids(&self) -> ThreadIdVec {
1602        self.threads.keys().copied().collect::<Vec<_>>().into()
1603    }
1604    
1605    // Cursor Blinking Timer
1606    
1607    /// Create the cursor blink timer
1608    ///
1609    /// This timer toggles cursor visibility at ~530ms intervals.
1610    /// It checks if enough time has passed since the last user input before blinking,
1611    /// to avoid blinking while the user is actively typing.
1612    pub fn create_cursor_blink_timer(&self, _window_state: &FullWindowState) -> crate::timer::Timer {
1613        use azul_core::task::{Duration, SystemTimeDiff};
1614        use crate::timer::{Timer, TimerCallback};
1615        use azul_core::refany::RefAny;
1616        
1617        let interval_ms = crate::managers::cursor::CURSOR_BLINK_INTERVAL_MS;
1618        
1619        // Create a RefAny with a unit type - the timer callback doesn't need any data
1620        // The actual cursor state is in LayoutWindow.cursor_manager
1621        let refany = RefAny::new(());
1622        
1623        Timer {
1624            refany,
1625            node_id: None.into(),
1626            created: azul_core::task::Instant::now(),
1627            run_count: 0,
1628            last_run: azul_core::task::OptionInstant::None,
1629            delay: azul_core::task::OptionDuration::None,
1630            interval: azul_core::task::OptionDuration::Some(Duration::System(SystemTimeDiff::from_millis(interval_ms))),
1631            timeout: azul_core::task::OptionDuration::None,
1632            callback: TimerCallback::create(cursor_blink_timer_callback),
1633        }
1634    }
1635    
1636    /// Scroll the active text cursor into view within its scrollable container
1637    ///
1638    /// This finds the focused contenteditable node, gets the cursor rectangle,
1639    /// and scrolls any scrollable ancestor to ensure the cursor is visible.
1640    pub fn scroll_active_cursor_into_view(&mut self, result: &mut CallbackChangeResult) {
1641        use crate::managers::scroll_into_view;
1642        
1643        // Get the focused node
1644        let focused_node = match self.focus_manager.get_focused_node() {
1645            Some(node) => *node,
1646            None => return,
1647        };
1648        
1649        let Some(node_id_internal) = focused_node.node.into_crate_internal() else {
1650            return;
1651        };
1652        
1653        // Check if node is contenteditable
1654        if !self.is_node_contenteditable_internal(focused_node.dom, node_id_internal) {
1655            return;
1656        }
1657        
1658        // Get the cursor location
1659        let cursor_location = match self.cursor_manager.get_cursor_location() {
1660            Some(loc) if loc.dom_id == focused_node.dom && loc.node_id == node_id_internal => loc,
1661            _ => return,
1662        };
1663        
1664        // Get the cursor position
1665        let cursor = match self.cursor_manager.get_cursor() {
1666            Some(c) => c.clone(),
1667            None => return,
1668        };
1669        
1670        // Get the inline layout to find the cursor rectangle
1671        let layout = match self.get_inline_layout_for_node(focused_node.dom, node_id_internal) {
1672            Some(l) => l,
1673            None => return,
1674        };
1675        
1676        // Get cursor rectangle (node-local coordinates)
1677        let cursor_rect = match layout.get_cursor_rect(&cursor) {
1678            Some(r) => r,
1679            None => return,
1680        };
1681        
1682        // Use scroll_into_view to scroll the cursor rect into view
1683        let now = azul_core::task::Instant::now();
1684        let options = scroll_into_view::ScrollIntoViewOptions::nearest();
1685        
1686        // Calculate scroll adjustments
1687        let adjustments = scroll_into_view::scroll_rect_into_view(
1688            cursor_rect,
1689            focused_node.dom,
1690            node_id_internal,
1691            &self.layout_results,
1692            &mut self.scroll_manager,
1693            options,
1694            now,
1695        );
1696        
1697        // Record the scroll changes
1698        for adj in adjustments {
1699            let current_pos = self.scroll_manager
1700                .get_current_offset(adj.scroll_container_dom_id, adj.scroll_container_node_id)
1701                .unwrap_or(LogicalPosition::zero());
1702            
1703            let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(adj.scroll_container_node_id));
1704            result
1705                .nodes_scrolled
1706                .entry(adj.scroll_container_dom_id)
1707                .or_insert_with(BTreeMap::new)
1708                .insert(hierarchy_id, current_pos);
1709        }
1710    }
1711    
1712    /// Check if a node is contenteditable (internal version using NodeId)
1713    fn is_node_contenteditable_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
1714        use crate::solver3::getters::is_node_contenteditable;
1715        
1716        let Some(layout_result) = self.layout_results.get(&dom_id) else {
1717            return false;
1718        };
1719        
1720        is_node_contenteditable(&layout_result.styled_dom, node_id)
1721    }
1722    
1723    /// Check if a node is contenteditable with W3C-conformant inheritance.
1724    ///
1725    /// This traverses up the DOM tree to check if the node or any ancestor
1726    /// has `contenteditable="true"` set, respecting `contenteditable="false"`
1727    /// to stop inheritance.
1728    fn is_node_contenteditable_inherited_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
1729        use crate::solver3::getters::is_node_contenteditable_inherited;
1730        
1731        let Some(layout_result) = self.layout_results.get(&dom_id) else {
1732            return false;
1733        };
1734        
1735        is_node_contenteditable_inherited(&layout_result.styled_dom, node_id)
1736    }
1737    
1738    /// Handle focus change for cursor blink timer management (W3C "flag and defer" pattern)
1739    ///
1740    /// This method implements the W3C focus/selection model:
1741    /// 1. Focus change is handled immediately (timer start/stop)
1742    /// 2. Cursor initialization is DEFERRED until after layout (via flag)
1743    ///
1744    /// The cursor is NOT initialized here because text layout may not be available
1745    /// during focus event handling. Instead, we set a flag that is consumed by
1746    /// `finalize_pending_focus_changes()` after the layout pass.
1747    ///
1748    /// # Parameters
1749    ///
1750    /// * `new_focus` - The newly focused node (None if focus is being cleared)
1751    /// * `current_window_state` - Current window state for timer creation
1752    ///
1753    /// # Returns
1754    ///
1755    /// A `CursorBlinkTimerAction` indicating what timer action the platform
1756    /// layer should take.
1757    pub fn handle_focus_change_for_cursor_blink(
1758        &mut self,
1759        new_focus: Option<azul_core::dom::DomNodeId>,
1760        current_window_state: &FullWindowState,
1761    ) -> CursorBlinkTimerAction {
1762        // Check if the new focus is on a contenteditable element
1763        // Use the inherited check for W3C conformance
1764        let contenteditable_info = match new_focus {
1765            Some(focus_node) => {
1766                if let Some(node_id) = focus_node.node.into_crate_internal() {
1767                    // Check if this node or any ancestor is contenteditable
1768                    if self.is_node_contenteditable_inherited_internal(focus_node.dom, node_id) {
1769                        // Find the text node where the cursor should be placed
1770                        let text_node_id = self.find_last_text_child(focus_node.dom, node_id)
1771                            .unwrap_or(node_id);
1772                        Some((focus_node.dom, node_id, text_node_id))
1773                    } else {
1774                        None
1775                    }
1776                } else {
1777                    None
1778                }
1779            }
1780            None => None,
1781        };
1782        
1783        // Determine the action based on current state and new focus
1784        let timer_was_active = self.cursor_manager.is_blink_timer_active();
1785        
1786        if let Some((dom_id, container_node_id, text_node_id)) = contenteditable_info {
1787            
1788            // W3C "flag and defer" pattern:
1789            // Set flag for cursor initialization AFTER layout pass
1790            self.focus_manager.set_pending_contenteditable_focus(
1791                dom_id,
1792                container_node_id,
1793                text_node_id,
1794            );
1795            
1796            // Make cursor visible and record current time (even before actual initialization)
1797            let now = azul_core::task::Instant::now();
1798            self.cursor_manager.reset_blink_on_input(now);
1799            self.cursor_manager.set_blink_timer_active(true);
1800            
1801            if !timer_was_active {
1802                // Need to start the timer
1803                let timer = self.create_cursor_blink_timer(current_window_state);
1804                return CursorBlinkTimerAction::Start(timer);
1805            } else {
1806                // Timer already active, just continue
1807                return CursorBlinkTimerAction::NoChange;
1808            }
1809        } else {
1810            // Focus is moving away from contenteditable or being cleared
1811            
1812            // Clear the cursor AND the pending focus flag
1813            self.cursor_manager.clear();
1814            self.focus_manager.clear_pending_contenteditable_focus();
1815            
1816            if timer_was_active {
1817                // Need to stop the timer
1818                self.cursor_manager.set_blink_timer_active(false);
1819                return CursorBlinkTimerAction::Stop;
1820            } else {
1821                return CursorBlinkTimerAction::NoChange;
1822            }
1823        }
1824    }
1825    
1826    /// Finalize pending focus changes after layout pass (W3C "flag and defer" pattern)
1827    ///
1828    /// This method should be called AFTER the layout pass completes. It checks if
1829    /// there's a pending contenteditable focus and initializes the cursor now that
1830    /// text layout information is available.
1831    ///
1832    /// # W3C Conformance
1833    ///
1834    /// In the W3C model:
1835    /// 1. Focus event fires during event handling (layout may not be ready)
1836    /// 2. Selection/cursor placement happens after layout is computed
1837    /// 3. The cursor is drawn at the position specified by the Selection
1838    ///
1839    /// This function implements step 2+3 by:
1840    /// - Checking the `cursor_needs_initialization` flag
1841    /// - Getting the (now available) text layout
1842    /// - Initializing the cursor at the correct position
1843    ///
1844    /// # Returns
1845    ///
1846    /// `true` if cursor was initialized, `false` if no pending focus or initialization failed.
1847    pub fn finalize_pending_focus_changes(&mut self) -> bool {
1848        // Take the pending focus info (this clears the flag)
1849        let pending = match self.focus_manager.take_pending_contenteditable_focus() {
1850            Some(p) => p,
1851            None => return false,
1852        };
1853        
1854        // Now we can safely get the text layout (layout pass has completed)
1855        let text_layout = self.get_inline_layout_for_node(pending.dom_id, pending.text_node_id).cloned();
1856        
1857        // Initialize cursor at end of text
1858        self.cursor_manager.initialize_cursor_at_end(
1859            pending.dom_id,
1860            pending.text_node_id,
1861            text_layout.as_ref(),
1862        )
1863    }
1864
1865    // CallbackChange Processing
1866
1867    /// Apply callback changes that were collected during callback execution
1868    ///
1869    /// This method processes all changes accumulated in the CallbackChange vector
1870    /// and applies them to the appropriate state. This is called after a callback
1871    /// returns to ensure atomic application of all changes.
1872    ///
1873    /// Returns a `CallbackChangeResult` containing all the changes to be applied.
1874    pub fn apply_callback_changes(
1875        &mut self,
1876        changes: Vec<crate::callbacks::CallbackChange>,
1877        current_window_state: &FullWindowState,
1878        image_cache: &mut ImageCache,
1879        system_fonts: &mut FcFontCache,
1880    ) -> CallbackChangeResult {
1881        use crate::callbacks::CallbackChange;
1882
1883        let mut result = CallbackChangeResult {
1884            modified_window_state: current_window_state.clone(),
1885            ..Default::default()
1886        };
1887        for change in changes {
1888            match change {
1889                CallbackChange::ModifyWindowState { state } => {
1890                    result.modified_window_state = state;
1891                }
1892                CallbackChange::QueueWindowStateSequence { states } => {
1893                    // Queue the states to be processed in sequence.
1894                    // The first state is applied immediately, subsequent states
1895                    // are stored for processing in future frames.
1896                    result.queued_window_states.extend(states);
1897                }
1898                CallbackChange::CreateNewWindow { options } => {
1899                    result.windows_created.push(options);
1900                }
1901                CallbackChange::CloseWindow => {
1902                    // Set the close_requested flag to trigger window close
1903                    result.modified_window_state.flags.close_requested = true;
1904                }
1905                CallbackChange::SetFocusTarget { target } => {
1906                    result.focus_target = Some(target);
1907                }
1908                CallbackChange::StopPropagation => {
1909                    result.stop_propagation = true;
1910                }
1911                CallbackChange::StopImmediatePropagation => {
1912                    result.stop_immediate_propagation = true;
1913                    result.stop_propagation = true; // implies stop_propagation
1914                }
1915                CallbackChange::PreventDefault => {
1916                    result.prevent_default = true;
1917                }
1918                CallbackChange::AddTimer { timer_id, timer } => {
1919                    result.timers.insert(timer_id, timer);
1920                }
1921                CallbackChange::RemoveTimer { timer_id } => {
1922                    result.timers_removed.insert(timer_id);
1923                }
1924                CallbackChange::AddThread { thread_id, thread } => {
1925                    result.threads.insert(thread_id, thread);
1926                }
1927                CallbackChange::RemoveThread { thread_id } => {
1928                    result.threads_removed.insert(thread_id);
1929                }
1930                CallbackChange::ChangeNodeText { node_id, text } => {
1931                    let dom_id = node_id.dom;
1932                    let internal_node_id = match node_id.node.into_crate_internal() {
1933                        Some(id) => id,
1934                        None => continue,
1935                    };
1936                    result
1937                        .words_changed
1938                        .entry(dom_id)
1939                        .or_insert_with(BTreeMap::new)
1940                        .insert(internal_node_id, text);
1941                }
1942                CallbackChange::ChangeNodeImage {
1943                    dom_id,
1944                    node_id,
1945                    image,
1946                    update_type,
1947                } => {
1948                    result
1949                        .images_changed
1950                        .entry(dom_id)
1951                        .or_insert_with(BTreeMap::new)
1952                        .insert(node_id, (image, update_type));
1953                }
1954                CallbackChange::UpdateImageCallback { dom_id, node_id } => {
1955                    result
1956                        .image_callbacks_changed
1957                        .entry(dom_id)
1958                        .or_insert_with(FastBTreeSet::new)
1959                        .insert(node_id);
1960                }
1961                CallbackChange::UpdateIFrame { dom_id, node_id } => {
1962                    result
1963                        .iframes_to_update
1964                        .entry(dom_id)
1965                        .or_insert_with(FastBTreeSet::new)
1966                        .insert(node_id);
1967                }
1968                CallbackChange::ChangeNodeImageMask {
1969                    dom_id,
1970                    node_id,
1971                    mask,
1972                } => {
1973                    result
1974                        .image_masks_changed
1975                        .entry(dom_id)
1976                        .or_insert_with(BTreeMap::new)
1977                        .insert(node_id, mask);
1978                }
1979                CallbackChange::ChangeNodeCssProperties {
1980                    dom_id,
1981                    node_id,
1982                    properties,
1983                } => {
1984                    result
1985                        .css_properties_changed
1986                        .entry(dom_id)
1987                        .or_insert_with(BTreeMap::new)
1988                        .insert(node_id, properties);
1989                }
1990                CallbackChange::ScrollTo {
1991                    dom_id,
1992                    node_id,
1993                    position,
1994                } => {
1995                    result
1996                        .nodes_scrolled
1997                        .entry(dom_id)
1998                        .or_insert_with(BTreeMap::new)
1999                        .insert(node_id, position);
2000                }
2001                CallbackChange::ScrollIntoView { node_id, options } => {
2002                    // Use the scroll_into_view module to calculate and apply scroll adjustments
2003                    use crate::managers::scroll_into_view;
2004                    let now = azul_core::task::Instant::now();
2005                    let adjustments = scroll_into_view::scroll_node_into_view(
2006                        node_id,
2007                        &self.layout_results,
2008                        &mut self.scroll_manager,
2009                        options,
2010                        now,
2011                    );
2012                    // Record the scroll changes in nodes_scrolled
2013                    // The scroll_manager was already updated by scroll_node_into_view,
2014                    // but we need to report the new absolute positions for event processing
2015                    for adj in adjustments {
2016                        // Get the current scroll position from scroll_manager (now updated)
2017                        let current_pos = self.scroll_manager
2018                            .get_current_offset(adj.scroll_container_dom_id, adj.scroll_container_node_id)
2019                            .unwrap_or(LogicalPosition::zero());
2020                        
2021                        let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(adj.scroll_container_node_id));
2022                        result
2023                            .nodes_scrolled
2024                            .entry(adj.scroll_container_dom_id)
2025                            .or_insert_with(BTreeMap::new)
2026                            .insert(hierarchy_id, current_pos);
2027                    }
2028                }
2029                CallbackChange::AddImageToCache { id, image } => {
2030                    image_cache.add_css_image_id(id, image);
2031                }
2032                CallbackChange::RemoveImageFromCache { id } => {
2033                    image_cache.delete_css_image_id(&id);
2034                }
2035                CallbackChange::ReloadSystemFonts => {
2036                    *system_fonts = FcFontCache::build();
2037                }
2038                CallbackChange::OpenMenu { menu, position } => {
2039                    result.menus_to_open.push((menu, position));
2040                }
2041                CallbackChange::ShowTooltip { text, position } => {
2042                    result.tooltips_to_show.push((text, position));
2043                }
2044                CallbackChange::HideTooltip => {
2045                    result.hide_tooltip = true;
2046                }
2047                CallbackChange::InsertText {
2048                    dom_id,
2049                    node_id,
2050                    text,
2051                } => {
2052                    // Record text input for the node
2053                    let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
2054                    let dom_node_id = DomNodeId {
2055                        dom: dom_id,
2056                        node: hierarchy_id,
2057                    };
2058
2059                    // Get old text from node
2060                    let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
2061                    let old_text = self.extract_text_from_inline_content(&old_inline_content);
2062
2063                    // Record the text input
2064                    use crate::managers::text_input::TextInputSource;
2065                    self.text_input_manager.record_input(
2066                        dom_node_id,
2067                        text.to_string(),
2068                        old_text,
2069                        TextInputSource::Programmatic,
2070                    );
2071                }
2072                CallbackChange::DeleteBackward { dom_id, node_id } => {
2073                    // Get current cursor/selection
2074                    if let Some(cursor) = self.cursor_manager.get_cursor() {
2075                        // Get current content
2076                        let content = self.get_text_before_textinput(dom_id, node_id);
2077
2078                        // Apply delete backward using text3::edit
2079
2080                        use crate::text3::edit::{delete_backward, TextEdit};
2081                        let mut new_content = content.clone();
2082                        let (updated_content, new_cursor) =
2083                            delete_backward(&mut new_content, cursor);
2084
2085                        // Update cursor position
2086                        self.cursor_manager
2087                            .move_cursor_to(new_cursor, dom_id, node_id);
2088
2089                        // Update text cache
2090                        self.update_text_cache_after_edit(dom_id, node_id, updated_content);
2091
2092                        // Mark node as dirty
2093                        let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
2094                        let dom_node_id = DomNodeId {
2095                            dom: dom_id,
2096                            node: hierarchy_id,
2097                        };
2098                        // Note: Dirty marking happens in the caller
2099                    }
2100                }
2101                CallbackChange::DeleteForward { dom_id, node_id } => {
2102                    // Get current cursor/selection
2103                    if let Some(cursor) = self.cursor_manager.get_cursor() {
2104                        // Get current content
2105                        let content = self.get_text_before_textinput(dom_id, node_id);
2106
2107                        // Apply delete forward using text3::edit
2108
2109                        use crate::text3::edit::{delete_forward, TextEdit};
2110                        let mut new_content = content.clone();
2111                        let (updated_content, new_cursor) =
2112                            delete_forward(&mut new_content, cursor);
2113
2114                        // Update cursor position
2115                        self.cursor_manager
2116                            .move_cursor_to(new_cursor, dom_id, node_id);
2117
2118                        // Update text cache
2119                        self.update_text_cache_after_edit(dom_id, node_id, updated_content);
2120                    }
2121                }
2122                CallbackChange::MoveCursor {
2123                    dom_id,
2124                    node_id,
2125                    cursor,
2126                } => {
2127                    // Update cursor position in CursorManager
2128                    self.cursor_manager.move_cursor_to(cursor, dom_id, node_id);
2129                }
2130                CallbackChange::SetSelection {
2131                    dom_id,
2132                    node_id,
2133                    selection,
2134                } => {
2135                    // Update selection in SelectionManager
2136                    let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
2137                    let dom_node_id = DomNodeId {
2138                        dom: dom_id,
2139                        node: hierarchy_id,
2140                    };
2141
2142                    match selection {
2143                        Selection::Cursor(cursor) => {
2144                            self.cursor_manager.move_cursor_to(cursor, dom_id, node_id);
2145                            self.selection_manager.clear_all();
2146                        }
2147                        Selection::Range(range) => {
2148                            self.cursor_manager
2149                                .move_cursor_to(range.start, dom_id, node_id);
2150                            // TODO: Set selection range in SelectionManager
2151                            // self.selection_manager.set_selection(dom_node_id, range);
2152                        }
2153                    }
2154                }
2155                CallbackChange::SetTextChangeset { changeset } => {
2156                    // Override the current text input changeset
2157                    // This allows user callbacks to modify what text will be inserted
2158                    self.text_input_manager.pending_changeset = Some(changeset);
2159                }
2160                // Cursor Movement Operations
2161                CallbackChange::MoveCursorLeft {
2162                    dom_id,
2163                    node_id,
2164                    extend_selection,
2165                } => {
2166                    if let Some(new_cursor) =
2167                        self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2168                            layout.move_cursor_left(*cursor, &mut None)
2169                        })
2170                    {
2171                        self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2172                    }
2173                }
2174                CallbackChange::MoveCursorRight {
2175                    dom_id,
2176                    node_id,
2177                    extend_selection,
2178                } => {
2179                    if let Some(new_cursor) =
2180                        self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2181                            layout.move_cursor_right(*cursor, &mut None)
2182                        })
2183                    {
2184                        self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2185                    }
2186                }
2187                CallbackChange::MoveCursorUp {
2188                    dom_id,
2189                    node_id,
2190                    extend_selection,
2191                } => {
2192                    if let Some(new_cursor) =
2193                        self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2194                            layout.move_cursor_up(*cursor, &mut None, &mut None)
2195                        })
2196                    {
2197                        self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2198                    }
2199                }
2200                CallbackChange::MoveCursorDown {
2201                    dom_id,
2202                    node_id,
2203                    extend_selection,
2204                } => {
2205                    if let Some(new_cursor) =
2206                        self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2207                            layout.move_cursor_down(*cursor, &mut None, &mut None)
2208                        })
2209                    {
2210                        self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2211                    }
2212                }
2213                CallbackChange::MoveCursorToLineStart {
2214                    dom_id,
2215                    node_id,
2216                    extend_selection,
2217                } => {
2218                    if let Some(new_cursor) =
2219                        self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2220                            layout.move_cursor_to_line_start(*cursor, &mut None)
2221                        })
2222                    {
2223                        self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2224                    }
2225                }
2226                CallbackChange::MoveCursorToLineEnd {
2227                    dom_id,
2228                    node_id,
2229                    extend_selection,
2230                } => {
2231                    if let Some(new_cursor) =
2232                        self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2233                            layout.move_cursor_to_line_end(*cursor, &mut None)
2234                        })
2235                    {
2236                        self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2237                    }
2238                }
2239                CallbackChange::MoveCursorToDocumentStart {
2240                    dom_id,
2241                    node_id,
2242                    extend_selection,
2243                } => {
2244                    // Document start is the first cluster in the layout
2245                    if let Some(new_cursor) = self.get_inline_layout_for_node(dom_id, node_id) {
2246                        if let Some(first_cluster) = new_cursor
2247                            .items
2248                            .first()
2249                            .and_then(|item| item.item.as_cluster())
2250                        {
2251                            let doc_start_cursor = TextCursor {
2252                                cluster_id: first_cluster.source_cluster_id,
2253                                affinity: CursorAffinity::Leading,
2254                            };
2255                            self.handle_cursor_movement(
2256                                dom_id,
2257                                node_id,
2258                                doc_start_cursor,
2259                                extend_selection,
2260                            );
2261                        }
2262                    }
2263                }
2264                CallbackChange::MoveCursorToDocumentEnd {
2265                    dom_id,
2266                    node_id,
2267                    extend_selection,
2268                } => {
2269                    // Document end is the last cluster in the layout
2270                    if let Some(layout) = self.get_inline_layout_for_node(dom_id, node_id) {
2271                        if let Some(last_cluster) =
2272                            layout.items.last().and_then(|item| item.item.as_cluster())
2273                        {
2274                            let doc_end_cursor = TextCursor {
2275                                cluster_id: last_cluster.source_cluster_id,
2276                                affinity: CursorAffinity::Trailing,
2277                            };
2278                            self.handle_cursor_movement(
2279                                dom_id,
2280                                node_id,
2281                                doc_end_cursor,
2282                                extend_selection,
2283                            );
2284                        }
2285                    }
2286                }
2287                // Clipboard Operations (Override)
2288                CallbackChange::SetCopyContent { target, content } => {
2289                    // Store clipboard content to be written to system clipboard
2290                    // This will be picked up by the platform's sync_clipboard() method
2291                    self.clipboard_manager.set_copy_content(content);
2292                }
2293                CallbackChange::SetCutContent { target, content } => {
2294                    // Same as copy, but the deletion is handled separately
2295                    self.clipboard_manager.set_copy_content(content);
2296                }
2297                CallbackChange::SetSelectAllRange { target, range } => {
2298                    // Override selection range for select-all operation
2299                    // Convert DomNodeId back to internal NodeId
2300                    if let Some(node_id_internal) = target.node.into_crate_internal() {
2301                        let dom_node_id = azul_core::dom::DomNodeId {
2302                            dom: target.dom,
2303                            node: target.node,
2304                        };
2305                        self.selection_manager
2306                            .set_range(target.dom, dom_node_id, range);
2307                    }
2308                }
2309                CallbackChange::RequestHitTestUpdate { position } => {
2310                    // Mark that a hit test update is requested
2311                    // This will be processed by the shell layer which has access to WebRender
2312                    result.hit_test_update_requested = Some(position);
2313                }
2314                CallbackChange::ProcessTextSelectionClick { position, time_ms } => {
2315                    // Process text selection click at position
2316                    // This is used by the Debug API to trigger text selection directly
2317                    // The selection update will cause the display list to be regenerated
2318                    let _ = self.process_mouse_click_for_selection(position, time_ms);
2319                }
2320                CallbackChange::SetCursorVisibility { visible: _ } => {
2321                    // Timer callback sets visibility - check if we should blink or stay solid
2322                    let now = azul_core::task::Instant::now();
2323                    if self.cursor_manager.should_blink(&now) {
2324                        // Enough time has passed since last input - toggle visibility
2325                        self.cursor_manager.toggle_visibility();
2326                    } else {
2327                        // User is actively typing - keep cursor visible
2328                        self.cursor_manager.set_visibility(true);
2329                    }
2330                }
2331                CallbackChange::ResetCursorBlink => {
2332                    // Reset cursor blink state on user input
2333                    let now = azul_core::task::Instant::now();
2334                    self.cursor_manager.reset_blink_on_input(now);
2335                }
2336                CallbackChange::StartCursorBlinkTimer => {
2337                    // Start the cursor blink timer if not already active
2338                    if !self.cursor_manager.is_blink_timer_active() {
2339                        let timer = self.create_cursor_blink_timer(current_window_state);
2340                        result.timers.insert(azul_core::task::CURSOR_BLINK_TIMER_ID, timer);
2341                        self.cursor_manager.set_blink_timer_active(true);
2342                    }
2343                }
2344                CallbackChange::StopCursorBlinkTimer => {
2345                    // Stop the cursor blink timer
2346                    if self.cursor_manager.is_blink_timer_active() {
2347                        result.timers_removed.insert(azul_core::task::CURSOR_BLINK_TIMER_ID);
2348                        self.cursor_manager.set_blink_timer_active(false);
2349                    }
2350                }
2351                CallbackChange::ScrollActiveCursorIntoView => {
2352                    // Scroll the active text cursor into view
2353                    self.scroll_active_cursor_into_view(&mut result);
2354                }
2355                CallbackChange::CreateTextInput { text } => {
2356                    // Create a synthetic text input event
2357                    // This simulates receiving text input from the OS
2358                    
2359                    // Process the text input - this records the changeset in TextInputManager
2360                    let affected_nodes = self.process_text_input(text.as_str());
2361                    
2362                    // Mark that we need to trigger text input callbacks
2363                    // The affected nodes and their events will be processed by the recursive event loop
2364                    for (node, (events, _)) in affected_nodes {
2365                        result.text_input_triggered.push((node, events));
2366                    }
2367                }
2368                CallbackChange::BeginInteractiveMove => {
2369                    // Handled by the platform layer (Wayland: xdg_toplevel_move).
2370                    // Set a flag so the platform can pick it up after callback processing.
2371                    result.begin_interactive_move = true;
2372                }
2373                CallbackChange::SetDragData { mime_type, data } => {
2374                    // Set drag data for a MIME type on the active drag
2375                    if let Some(ctx) = self.gesture_drag_manager.get_drag_context_mut() {
2376                        if let Some(node_drag) = ctx.as_node_drag_mut() {
2377                            node_drag.drag_data.set_data(mime_type, data);
2378                        }
2379                    }
2380                }
2381                CallbackChange::AcceptDrop => {
2382                    // Mark the current drop as accepted
2383                    if let Some(ctx) = self.gesture_drag_manager.get_drag_context_mut() {
2384                        if let Some(node_drag) = ctx.as_node_drag_mut() {
2385                            node_drag.drop_accepted = true;
2386                        }
2387                    }
2388                }
2389                CallbackChange::SetDropEffect { effect } => {
2390                    // Set the drop effect on the active drag
2391                    if let Some(ctx) = self.gesture_drag_manager.get_drag_context_mut() {
2392                        if let Some(node_drag) = ctx.as_node_drag_mut() {
2393                            node_drag.drop_effect = effect;
2394                        }
2395                    }
2396                }
2397            }
2398        }
2399
2400        // Sync cursor to selection manager for rendering
2401        // This must happen after all cursor updates
2402        self.sync_cursor_to_selection_manager();
2403
2404        result
2405    }
2406
2407    /// Helper: Get inline layout for a node
2408    /// 
2409    /// For text nodes that participate in an IFC, the inline layout is stored
2410    /// on the IFC root node (the block container), not on the text node itself.
2411    /// This method handles both cases:
2412    /// 1. The node has its own `inline_layout_result` (IFC root)
2413    /// 2. The node has `ifc_membership` pointing to the IFC root
2414    ///
2415    /// This is a thin wrapper around `LayoutTree::get_inline_layout_for_node`.
2416    fn get_inline_layout_for_node(
2417        &self,
2418        dom_id: DomId,
2419        node_id: NodeId,
2420    ) -> Option<&Arc<UnifiedLayout>> {
2421        let layout_result = self.layout_results.get(&dom_id)?;
2422
2423        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
2424        let layout_index = *layout_indices.first()?;
2425
2426        // Use the centralized LayoutTree method that handles IFC membership
2427        layout_result.layout_tree.get_inline_layout_for_node(layout_index)
2428    }
2429
2430    /// Helper: Move cursor using a movement function and return the new cursor if it changed
2431    fn move_cursor_in_node<F>(
2432        &self,
2433        dom_id: DomId,
2434        node_id: NodeId,
2435        movement_fn: F,
2436    ) -> Option<TextCursor>
2437    where
2438        F: FnOnce(&UnifiedLayout, &TextCursor) -> TextCursor,
2439    {
2440        let current_cursor = self.cursor_manager.get_cursor()?;
2441        let layout = self.get_inline_layout_for_node(dom_id, node_id)?;
2442
2443        let new_cursor = movement_fn(layout, current_cursor);
2444
2445        // Only return if cursor actually moved
2446        if new_cursor != *current_cursor {
2447            Some(new_cursor)
2448        } else {
2449            None
2450        }
2451    }
2452
2453    /// Helper: Handle cursor movement with optional selection extension
2454    fn handle_cursor_movement(
2455        &mut self,
2456        dom_id: DomId,
2457        node_id: NodeId,
2458        new_cursor: TextCursor,
2459        extend_selection: bool,
2460    ) {
2461        if extend_selection {
2462            // Get the current cursor as the selection anchor
2463            if let Some(old_cursor) = self.cursor_manager.get_cursor() {
2464                // Create DomNodeId for the selection
2465                let dom_node_id = azul_core::dom::DomNodeId {
2466                    dom: dom_id,
2467                    node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
2468                };
2469
2470                // Create a selection range from old cursor to new cursor
2471                let selection_range = if new_cursor.cluster_id.start_byte_in_run
2472                    < old_cursor.cluster_id.start_byte_in_run
2473                {
2474                    // Moving backwards
2475                    SelectionRange {
2476                        start: new_cursor,
2477                        end: *old_cursor,
2478                    }
2479                } else {
2480                    // Moving forwards
2481                    SelectionRange {
2482                        start: *old_cursor,
2483                        end: new_cursor,
2484                    }
2485                };
2486
2487                // Set the selection range in SelectionManager
2488                self.selection_manager
2489                    .set_range(dom_id, dom_node_id, selection_range);
2490            }
2491
2492            // Move cursor to new position
2493            self.cursor_manager
2494                .move_cursor_to(new_cursor, dom_id, node_id);
2495        } else {
2496            // Just move cursor without extending selection
2497            self.cursor_manager
2498                .move_cursor_to(new_cursor, dom_id, node_id);
2499
2500            // Clear any existing selection
2501            self.selection_manager.clear_selection(&dom_id);
2502        }
2503    }
2504
2505    // Gpu Value Cache Management
2506
2507    /// Get the GPU value cache for a specific DOM
2508    pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
2509        self.gpu_state_manager.caches.get(dom_id)
2510    }
2511
2512    /// Get a mutable reference to the GPU value cache for a specific DOM
2513    pub fn get_gpu_cache_mut(&mut self, dom_id: &DomId) -> Option<&mut GpuValueCache> {
2514        self.gpu_state_manager.caches.get_mut(dom_id)
2515    }
2516
2517    /// Get or create a GPU value cache for a specific DOM
2518    pub fn get_or_create_gpu_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
2519        self.gpu_state_manager
2520            .caches
2521            .entry(dom_id)
2522            .or_insert_with(GpuValueCache::default)
2523    }
2524
2525    // Layout Result Access
2526
2527    /// Get a layout result for a specific DOM
2528    pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
2529        self.layout_results.get(dom_id)
2530    }
2531
2532    /// Get a mutable layout result for a specific DOM
2533    pub fn get_layout_result_mut(&mut self, dom_id: &DomId) -> Option<&mut DomLayoutResult> {
2534        self.layout_results.get_mut(dom_id)
2535    }
2536
2537    /// Get all DOM IDs that have layout results
2538    pub fn get_dom_ids(&self) -> DomIdVec {
2539        self.layout_results
2540            .keys()
2541            .copied()
2542            .collect::<Vec<_>>()
2543            .into()
2544    }
2545
2546    // Hit-Test Computation
2547
2548    /// Compute the cursor type hit-test from a full hit-test
2549    ///
2550    /// This determines which mouse cursor to display based on the CSS cursor
2551    /// properties of the hovered nodes.
2552    pub fn compute_cursor_type_hit_test(
2553        &self,
2554        hit_test: &crate::hit_test::FullHitTest,
2555    ) -> crate::hit_test::CursorTypeHitTest {
2556        crate::hit_test::CursorTypeHitTest::new(hit_test, self)
2557    }
2558
2559    // TODO: Implement compute_hit_test() once we have the actual hit-testing logic
2560    // This would involve:
2561    // 1. Converting screen coordinates to layout coordinates
2562    // 2. Traversing the layout tree to find nodes under the cursor
2563    // 3. Handling z-index and stacking contexts
2564    // 4. Building the FullHitTest structure
2565
2566    /// Synchronize scrollbar opacity values with the GPU value cache.
2567    ///
2568    /// This method updates GPU opacity keys for all scrollbars based on scroll activity
2569    /// tracked by the ScrollManager. It enables smooth scrollbar fading without
2570    /// requiring display list regeneration.
2571    ///
2572    /// # Arguments
2573    ///
2574    /// * `dom_id` - The DOM to synchronize scrollbar opacity for
2575    /// * `layout_tree` - The layout tree containing scrollbar information
2576    /// * `now` - Current timestamp for calculating fade progress
2577    /// * `fade_delay` - Delay before scrollbar starts fading (e.g., 500ms)
2578    /// * `fade_duration` - Duration of the fade animation (e.g., 200ms)
2579    ///
2580    /// # Returns
2581    ///
2582    /// A vector of GPU scrollbar opacity change events
2583
2584    /// Helper function to calculate scrollbar opacity based on activity time
2585    fn calculate_scrollbar_opacity(
2586        last_activity: Option<Instant>,
2587        now: Instant,
2588        fade_delay: Duration,
2589        fade_duration: Duration,
2590    ) -> f32 {
2591        let Some(last_activity) = last_activity else {
2592            return 0.0;
2593        };
2594
2595        let time_since_activity = now.duration_since(&last_activity);
2596
2597        // Phase 1: Scrollbar stays fully visible during fade_delay
2598        if time_since_activity.div(&fade_delay) < 1.0 {
2599            return 1.0;
2600        }
2601
2602        // Phase 2: Fade out over fade_duration
2603        let time_into_fade_ms = time_since_activity.div(&fade_delay) - 1.0;
2604        let fade_progress = (time_into_fade_ms * fade_duration.div(&fade_duration)).min(1.0);
2605
2606        // Phase 3: Fully faded
2607        (1.0 - fade_progress).max(0.0)
2608    }
2609
2610    /// Synchronize scrollbar opacity values with the GPU value cache.
2611    ///
2612    /// Static method that takes individual components instead of &mut self to avoid borrow
2613    /// conflicts.
2614    pub fn synchronize_scrollbar_opacity(
2615        gpu_state_manager: &mut GpuStateManager,
2616        scroll_manager: &ScrollManager,
2617        dom_id: DomId,
2618        layout_tree: &LayoutTree,
2619        system_callbacks: &ExternalSystemCallbacks,
2620        fade_delay: azul_core::task::Duration,
2621        fade_duration: azul_core::task::Duration,
2622    ) -> Vec<azul_core::gpu::GpuScrollbarOpacityEvent> {
2623        let mut events = Vec::new();
2624        let gpu_cache = gpu_state_manager.caches.entry(dom_id).or_default();
2625
2626        // Get current time from system callbacks
2627        let now = (system_callbacks.get_system_time_fn.cb)();
2628
2629        // Iterate over all nodes with scrollbar info
2630        for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
2631            // Check if node needs scrollbars
2632            let scrollbar_info = match &node.scrollbar_info {
2633                Some(info) => info,
2634                None => continue,
2635            };
2636
2637            let node_id = match node.dom_node_id {
2638                Some(nid) => nid,
2639                None => continue, // Skip anonymous boxes
2640            };
2641
2642            // Calculate current opacity from ScrollManager
2643            let vertical_opacity = if scrollbar_info.needs_vertical {
2644                Self::calculate_scrollbar_opacity(
2645                    scroll_manager.get_last_activity_time(dom_id, node_id),
2646                    now.clone(),
2647                    fade_delay,
2648                    fade_duration,
2649                )
2650            } else {
2651                0.0
2652            };
2653
2654            let horizontal_opacity = if scrollbar_info.needs_horizontal {
2655                Self::calculate_scrollbar_opacity(
2656                    scroll_manager.get_last_activity_time(dom_id, node_id),
2657                    now.clone(),
2658                    fade_delay,
2659                    fade_duration,
2660                )
2661            } else {
2662                0.0
2663            };
2664
2665            // Handle vertical scrollbar
2666            if scrollbar_info.needs_vertical && vertical_opacity > 0.001 {
2667                let key = (dom_id, node_id);
2668                let existing = gpu_cache.scrollbar_v_opacity_values.get(&key);
2669
2670                match existing {
2671                    None => {
2672                        let opacity_key = OpacityKey::unique();
2673                        gpu_cache.scrollbar_v_opacity_keys.insert(key, opacity_key);
2674                        gpu_cache
2675                            .scrollbar_v_opacity_values
2676                            .insert(key, vertical_opacity);
2677                        events.push(GpuScrollbarOpacityEvent::VerticalAdded(
2678                            dom_id,
2679                            node_id,
2680                            opacity_key,
2681                            vertical_opacity,
2682                        ));
2683                    }
2684                    Some(&old_opacity) if (old_opacity - vertical_opacity).abs() > 0.001 => {
2685                        let opacity_key = gpu_cache.scrollbar_v_opacity_keys[&key];
2686                        gpu_cache
2687                            .scrollbar_v_opacity_values
2688                            .insert(key, vertical_opacity);
2689                        events.push(GpuScrollbarOpacityEvent::VerticalChanged(
2690                            dom_id,
2691                            node_id,
2692                            opacity_key,
2693                            old_opacity,
2694                            vertical_opacity,
2695                        ));
2696                    }
2697                    _ => {}
2698                }
2699            } else {
2700                // Remove if scrollbar no longer needed or fully transparent
2701                let key = (dom_id, node_id);
2702                if let Some(opacity_key) = gpu_cache.scrollbar_v_opacity_keys.remove(&key) {
2703                    gpu_cache.scrollbar_v_opacity_values.remove(&key);
2704                    events.push(GpuScrollbarOpacityEvent::VerticalRemoved(
2705                        dom_id,
2706                        node_id,
2707                        opacity_key,
2708                    ));
2709                }
2710            }
2711
2712            // Handle horizontal scrollbar (same logic)
2713            if scrollbar_info.needs_horizontal && horizontal_opacity > 0.001 {
2714                let key = (dom_id, node_id);
2715                let existing = gpu_cache.scrollbar_h_opacity_values.get(&key);
2716
2717                match existing {
2718                    None => {
2719                        let opacity_key = OpacityKey::unique();
2720                        gpu_cache.scrollbar_h_opacity_keys.insert(key, opacity_key);
2721                        gpu_cache
2722                            .scrollbar_h_opacity_values
2723                            .insert(key, horizontal_opacity);
2724                        events.push(GpuScrollbarOpacityEvent::HorizontalAdded(
2725                            dom_id,
2726                            node_id,
2727                            opacity_key,
2728                            horizontal_opacity,
2729                        ));
2730                    }
2731                    Some(&old_opacity) if (old_opacity - horizontal_opacity).abs() > 0.001 => {
2732                        let opacity_key = gpu_cache.scrollbar_h_opacity_keys[&key];
2733                        gpu_cache
2734                            .scrollbar_h_opacity_values
2735                            .insert(key, horizontal_opacity);
2736                        events.push(GpuScrollbarOpacityEvent::HorizontalChanged(
2737                            dom_id,
2738                            node_id,
2739                            opacity_key,
2740                            old_opacity,
2741                            horizontal_opacity,
2742                        ));
2743                    }
2744                    _ => {}
2745                }
2746            } else {
2747                // Remove if scrollbar no longer needed or fully transparent
2748                let key = (dom_id, node_id);
2749                if let Some(opacity_key) = gpu_cache.scrollbar_h_opacity_keys.remove(&key) {
2750                    gpu_cache.scrollbar_h_opacity_values.remove(&key);
2751                    events.push(GpuScrollbarOpacityEvent::HorizontalRemoved(
2752                        dom_id,
2753                        node_id,
2754                        opacity_key,
2755                    ));
2756                }
2757            }
2758        }
2759
2760        events
2761    }
2762
2763    /// Compute stable scroll IDs for all scrollable nodes in a layout tree
2764    ///
2765    /// This should be called after layout but before display list generation.
2766    /// It creates stable IDs based on node_data_hash that persist across frames.
2767    ///
2768    /// Returns:
2769    /// - scroll_ids: Map from layout node index -> external scroll ID
2770    /// - scroll_id_to_node_id: Map from scroll ID -> DOM NodeId (for hit testing)
2771    pub fn compute_scroll_ids(
2772        layout_tree: &LayoutTree,
2773        styled_dom: &azul_core::styled_dom::StyledDom,
2774    ) -> (BTreeMap<usize, u64>, BTreeMap<u64, NodeId>) {
2775        use azul_css::props::layout::LayoutOverflow;
2776
2777        use crate::solver3::getters::{get_overflow_x, get_overflow_y};
2778
2779        let mut scroll_ids = BTreeMap::new();
2780        let mut scroll_id_to_node_id = BTreeMap::new();
2781
2782        // Iterate through all layout nodes
2783        for (layout_idx, node) in layout_tree.nodes.iter().enumerate() {
2784            let Some(dom_node_id) = node.dom_node_id else {
2785                continue;
2786            };
2787
2788            // Get the node state
2789            let styled_node_state = styled_dom
2790                .styled_nodes
2791                .as_container()
2792                .get(dom_node_id)
2793                .map(|n| n.styled_node_state.clone())
2794                .unwrap_or_default();
2795
2796            // Check if this node has scroll overflow
2797            let overflow_x = get_overflow_x(styled_dom, dom_node_id, &styled_node_state);
2798            let overflow_y = get_overflow_y(styled_dom, dom_node_id, &styled_node_state);
2799
2800            let is_scrollable = overflow_x.is_scroll() || overflow_y.is_scroll();
2801
2802            if !is_scrollable {
2803                continue;
2804            }
2805
2806            // Generate stable scroll ID from node_data_hash
2807            // Use node_data_hash to create a stable ID that persists across frames
2808            let scroll_id = node.node_data_hash;
2809
2810            scroll_ids.insert(layout_idx, scroll_id);
2811            scroll_id_to_node_id.insert(scroll_id, dom_node_id);
2812        }
2813
2814        (scroll_ids, scroll_id_to_node_id)
2815    }
2816
2817    /// Get the layout rectangle for a specific DOM node in logical coordinates
2818    ///
2819    /// This is useful in callbacks to get the position and size of the hit node
2820    /// for positioning menus, tooltips, or other overlays.
2821    ///
2822    /// Returns None if the node is not currently laid out (e.g., display:none)
2823    pub fn get_node_layout_rect(
2824        &self,
2825        node_id: azul_core::dom::DomNodeId,
2826    ) -> Option<azul_core::geom::LogicalRect> {
2827        // Get the layout tree from cache
2828        let layout_tree = self.layout_cache.tree.as_ref()?;
2829
2830        // Find the layout node index corresponding to this DOM node
2831        // Convert NodeHierarchyItemId to Option<NodeId> for comparison
2832        let target_node_id = node_id.node.into_crate_internal();
2833        let layout_idx = layout_tree
2834            .nodes
2835            .iter()
2836            .position(|node| node.dom_node_id == target_node_id)?;
2837
2838        // Get the calculated layout position from cache (already in logical units)
2839        let calc_pos = self.layout_cache.calculated_positions.get(layout_idx)?;
2840
2841        // Get the layout node for size information
2842        let layout_node = layout_tree.nodes.get(layout_idx)?;
2843
2844        // Get the used size (the actual laid-out size)
2845        let used_size = layout_node.used_size?;
2846
2847        // Convert size to logical coordinates
2848        let hidpi_factor = self
2849            .current_window_state
2850            .size
2851            .get_hidpi_factor()
2852            .inner
2853            .get();
2854
2855        Some(LogicalRect::new(
2856            LogicalPosition::new(calc_pos.x as f32, calc_pos.y as f32),
2857            LogicalSize::new(
2858                used_size.width / hidpi_factor,
2859                used_size.height / hidpi_factor,
2860            ),
2861        ))
2862    }
2863
2864    /// Get the cursor rect for the currently focused text input node in ABSOLUTE coordinates.
2865    ///
2866    /// This returns the cursor position in absolute window coordinates (not accounting for
2867    /// scroll offsets). This is used for scroll-into-view calculations where you need to
2868    /// compare the cursor position with the scrollable container's bounds.
2869    ///
2870    /// Returns None if:
2871    /// - No node is focused
2872    /// - Focused node has no text cursor
2873    /// - Focused node has no layout
2874    /// - Text cache cannot find cursor position
2875    ///
2876    /// For IME positioning (viewport-relative coordinates), use
2877    /// `get_focused_cursor_rect_viewport()`.
2878    pub fn get_focused_cursor_rect(&self) -> Option<azul_core::geom::LogicalRect> {
2879        // Get the focused node
2880        let focused_node = self.focus_manager.focused_node?;
2881
2882        // Get the text cursor
2883        let cursor = self.cursor_manager.get_cursor()?;
2884
2885        // Get the layout tree from cache
2886        let layout_tree = self.layout_cache.tree.as_ref()?;
2887
2888        // Find the layout node index corresponding to the focused DOM node
2889        let target_node_id = focused_node.node.into_crate_internal();
2890        let layout_idx = layout_tree
2891            .nodes
2892            .iter()
2893            .position(|node| node.dom_node_id == target_node_id)?;
2894
2895        // Get the layout node
2896        let layout_node = layout_tree.nodes.get(layout_idx)?;
2897
2898        // Get the text layout result for this node
2899        let cached_layout = layout_node.inline_layout_result.as_ref()?;
2900        let inline_layout = &cached_layout.layout;
2901
2902        // Get the cursor rect in node-relative coordinates
2903        let mut cursor_rect = inline_layout.get_cursor_rect(cursor)?;
2904
2905        // Get the calculated layout position from cache (already in logical units)
2906        let calc_pos = self.layout_cache.calculated_positions.get(layout_idx)?;
2907
2908        // Add layout position to cursor rect (both already in logical units)
2909        cursor_rect.origin.x += calc_pos.x as f32;
2910        cursor_rect.origin.y += calc_pos.y as f32;
2911
2912        // Return ABSOLUTE position (no scroll correction)
2913        Some(cursor_rect)
2914    }
2915
2916    /// Get the cursor rect for the currently focused text input node in VIEWPORT coordinates.
2917    ///
2918    /// This returns the cursor position accounting for:
2919    /// 1. Scroll offsets from all scrollable ancestors
2920    /// 2. GPU transforms (CSS transforms, animations) from all transformed ancestors
2921    ///
2922    /// The returned position is viewport-relative (what the user actually sees on screen).
2923    /// This is used for IME window positioning, where the IME popup needs to appear at the
2924    /// visible cursor location, not the absolute layout position.
2925    ///
2926    /// Returns None if:
2927    /// - No node is focused
2928    /// - Focused node has no text cursor
2929    /// - Focused node has no layout
2930    /// - Text cache cannot find cursor position
2931    ///
2932    /// For scroll-into-view calculations (absolute coordinates), use `get_focused_cursor_rect()`.
2933    pub fn get_focused_cursor_rect_viewport(&self) -> Option<azul_core::geom::LogicalRect> {
2934        // Start with absolute position
2935        let mut cursor_rect = self.get_focused_cursor_rect()?;
2936
2937        // Get the focused node
2938        let focused_node = self.focus_manager.focused_node?;
2939
2940        // Get the layout tree from cache
2941        let layout_tree = self.layout_cache.tree.as_ref()?;
2942
2943        // Find the layout node index corresponding to the focused DOM node
2944        let target_node_id = focused_node.node.into_crate_internal();
2945        let layout_idx = layout_tree
2946            .nodes
2947            .iter()
2948            .position(|node| node.dom_node_id == target_node_id)?;
2949
2950        // Get the GPU cache for this DOM (if it exists)
2951        let gpu_cache = self.gpu_state_manager.caches.get(&focused_node.dom);
2952
2953        // CRITICAL STEP 1: Apply scroll offsets from all scrollable ancestors
2954        // CRITICAL STEP 2: Apply inverse GPU transforms from all transformed ancestors
2955        // Walk up the tree and apply both corrections
2956        let mut current_layout_idx = layout_idx;
2957
2958        while let Some(parent_idx) = layout_tree.nodes.get(current_layout_idx)?.parent {
2959            // Get the DOM node ID of the parent (if it's not anonymous)
2960            if let Some(parent_dom_node_id) = layout_tree.nodes.get(parent_idx)?.dom_node_id {
2961                // STEP 1: Check if this parent is scrollable and has scroll state
2962                if let Some(scroll_state) = self
2963                    .scroll_manager
2964                    .get_scroll_state(focused_node.dom, parent_dom_node_id)
2965                {
2966                    // Subtract scroll offset (scrolling down = positive offset, moves content up)
2967                    cursor_rect.origin.x -= scroll_state.current_offset.x;
2968                    cursor_rect.origin.y -= scroll_state.current_offset.y;
2969                }
2970
2971                // STEP 2: Check if this parent has a GPU transform applied
2972                if let Some(cache) = gpu_cache {
2973                    if let Some(transform) = cache.current_transform_values.get(&parent_dom_node_id)
2974                    {
2975                        // Apply the INVERSE transform to get back to viewport coordinates
2976                        // The transform moves the element, so we need to reverse it for the cursor
2977                        let inverse = transform.inverse();
2978                        if let Some(transformed_origin) =
2979                            inverse.transform_point2d(cursor_rect.origin)
2980                        {
2981                            cursor_rect.origin = transformed_origin;
2982                        }
2983                        // Note: We don't transform the size, only the position
2984                    }
2985                }
2986            }
2987
2988            // Move to parent for next iteration
2989            current_layout_idx = parent_idx;
2990        }
2991
2992        Some(cursor_rect)
2993    }
2994
2995    /// Find the nearest scrollable ancestor for a given node
2996    /// Returns (DomId, NodeId) of the scrollable container, or None if no scrollable ancestor
2997    /// exists
2998    pub fn find_scrollable_ancestor(
2999        &self,
3000        mut node_id: azul_core::dom::DomNodeId,
3001    ) -> Option<azul_core::dom::DomNodeId> {
3002        // Get the layout tree
3003        let layout_tree = self.layout_cache.tree.as_ref()?;
3004
3005        // Convert to internal NodeId
3006        let mut current_node_id = node_id.node.into_crate_internal();
3007
3008        // Walk up the tree looking for a scrollable node
3009        loop {
3010            // Find layout node index
3011            let layout_idx = layout_tree
3012                .nodes
3013                .iter()
3014                .position(|node| node.dom_node_id == current_node_id)?;
3015
3016            let layout_node = layout_tree.nodes.get(layout_idx)?;
3017
3018            // Check if this node has scrollbar info (meaning it's scrollable)
3019            if layout_node.scrollbar_info.is_some() {
3020                // Check if it actually has a scroll state registered
3021                let check_node_id = current_node_id?;
3022                if self
3023                    .scroll_manager
3024                    .get_scroll_state(node_id.dom, check_node_id)
3025                    .is_some()
3026                {
3027                    // Found a scrollable ancestor
3028                    return Some(azul_core::dom::DomNodeId {
3029                        dom: node_id.dom,
3030                        node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(
3031                            Some(check_node_id),
3032                        ),
3033                    });
3034                }
3035            }
3036
3037            // Move to parent
3038            let parent_idx = layout_node.parent?;
3039            let parent_node = layout_tree.nodes.get(parent_idx)?;
3040            current_node_id = parent_node.dom_node_id;
3041        }
3042    }
3043
3044    /// Scroll selection or cursor into view with distance-based acceleration.
3045    ///
3046    /// **Unified Scroll System**: This method handles both cursor (0-size selection)
3047    /// and full selection scrolling with a single implementation. For drag-to-scroll,
3048    /// scroll speed increases with distance from container edge.
3049    ///
3050    /// ## Algorithm
3051    /// 1. Get bounds to scroll (cursor rect, selection rect, or mouse position)
3052    /// 2. Find scrollable ancestor container
3053    /// 3. Calculate distance from bounds to container edges
3054    /// 4. Compute scroll delta (instant with padding, or accelerated with zones)
3055    /// 5. Apply scroll with appropriate animation
3056    ///
3057    /// ## Distance-Based Acceleration (ScrollMode::Accelerated)
3058    /// ```text
3059    /// Distance from edge:  Scroll speed per frame:
3060    /// 0-20px              Dead zone (no scroll)
3061    /// 20-50px             Slow (2px/frame)
3062    /// 50-100px            Medium (4px/frame)
3063    /// 100-200px           Fast (8px/frame)
3064    /// 200+px              Very fast (16px/frame)
3065    /// ```
3066    ///
3067    /// ## Returns
3068    /// `true` if scrolling was applied, `false` if already visible
3069    pub fn scroll_selection_into_view(
3070        &mut self,
3071        scroll_type: SelectionScrollType,
3072        scroll_mode: ScrollMode,
3073    ) -> bool {
3074        // Get bounds to scroll into view
3075        let bounds = match scroll_type {
3076            SelectionScrollType::Cursor => {
3077                // Cursor is 0-size selection at insertion point
3078                match self.get_focused_cursor_rect() {
3079                    Some(rect) => rect,
3080                    None => return false, // No cursor to scroll
3081                }
3082            }
3083            SelectionScrollType::Selection => {
3084                // Get selection range(s) and compute bounding rect
3085                // For now, treat as cursor until we implement calculate_selection_bounding_rect
3086                match self.get_focused_cursor_rect() {
3087                    Some(rect) => rect,
3088                    None => return false, // No selection to scroll
3089                }
3090                // TODO: Implement calculate_selection_bounding_rect
3091                // let ranges = self.selection_manager.get_selection();
3092                // if ranges.is_empty() {
3093                //     return false;
3094                // }
3095                // self.calculate_selection_bounding_rect(ranges)?
3096            }
3097            SelectionScrollType::DragSelection { mouse_position } => {
3098                // For drag: use mouse position to determine scroll direction/speed
3099                LogicalRect::new(mouse_position, LogicalSize::zero())
3100            }
3101        };
3102
3103        // Get the focused node (or bail if no focus)
3104        let focused_node = match self.focus_manager.focused_node {
3105            Some(node) => node,
3106            None => return false,
3107        };
3108
3109        // Find scrollable ancestor
3110        let scroll_container = match self.find_scrollable_ancestor(focused_node) {
3111            Some(node) => node,
3112            None => return false, // No scrollable ancestor
3113        };
3114
3115        // Get container bounds and current scroll state
3116        let layout_tree = match self.layout_cache.tree.as_ref() {
3117            Some(tree) => tree,
3118            None => return false,
3119        };
3120
3121        let scrollable_node_internal = match scroll_container.node.into_crate_internal() {
3122            Some(id) => id,
3123            None => return false,
3124        };
3125
3126        let layout_idx = match layout_tree
3127            .nodes
3128            .iter()
3129            .position(|n| n.dom_node_id == Some(scrollable_node_internal))
3130        {
3131            Some(idx) => idx,
3132            None => return false,
3133        };
3134
3135        let scrollable_layout_node = match layout_tree.nodes.get(layout_idx) {
3136            Some(node) => node,
3137            None => return false,
3138        };
3139
3140        let container_pos = self
3141            .layout_cache
3142            .calculated_positions
3143            .get(layout_idx)
3144            .copied()
3145            .unwrap_or_default();
3146
3147        let container_size = scrollable_layout_node.used_size.unwrap_or_default();
3148
3149        let container_rect = LogicalRect {
3150            origin: container_pos,
3151            size: container_size,
3152        };
3153
3154        // Get current scroll state
3155        let scroll_state = match self
3156            .scroll_manager
3157            .get_scroll_state(scroll_container.dom, scrollable_node_internal)
3158        {
3159            Some(state) => state,
3160            None => return false,
3161        };
3162
3163        // Calculate visible area (container rect adjusted by scroll offset)
3164        let visible_area = LogicalRect::new(
3165            LogicalPosition::new(
3166                container_rect.origin.x + scroll_state.current_offset.x,
3167                container_rect.origin.y + scroll_state.current_offset.y,
3168            ),
3169            container_rect.size,
3170        );
3171
3172        // Calculate scroll delta based on mode
3173        let scroll_delta = match scroll_mode {
3174            ScrollMode::Instant => {
3175                // For typing/clicking: instant scroll with fixed padding
3176                calculate_instant_scroll_delta(bounds, visible_area)
3177            }
3178            ScrollMode::Accelerated => {
3179                // For drag: accelerated scroll based on distance from edge
3180                let distance = calculate_edge_distance(bounds, visible_area);
3181                calculate_accelerated_scroll_delta(distance)
3182            }
3183        };
3184
3185        // Apply scroll if needed
3186        if scroll_delta.x != 0.0 || scroll_delta.y != 0.0 {
3187            let duration = match scroll_mode {
3188                ScrollMode::Instant => Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
3189                ScrollMode::Accelerated => Duration::System(SystemTimeDiff {
3190                    secs: 0,
3191                    nanos: 16_666_667,
3192                }), // 60fps
3193            };
3194
3195            let external = ExternalSystemCallbacks::rust_internal();
3196            let now = (external.get_system_time_fn.cb)();
3197
3198            // Calculate new scroll target
3199            let new_target = LogicalPosition {
3200                x: scroll_state.current_offset.x + scroll_delta.x,
3201                y: scroll_state.current_offset.y + scroll_delta.y,
3202            };
3203
3204            self.scroll_manager.scroll_to(
3205                scroll_container.dom,
3206                scrollable_node_internal,
3207                new_target,
3208                duration,
3209                EasingFunction::Linear,
3210                now.into(),
3211            );
3212
3213            true // Scrolled
3214        } else {
3215            false // Already visible
3216        }
3217    }
3218
3219    /// Automatically scrolls the focused cursor into view after layout.
3220    ///
3221    /// **DEPRECATED**: Use `scroll_selection_into_view(SelectionScrollType::Cursor,
3222    /// ScrollMode::Instant)` instead. This method is kept for compatibility but redirects to
3223    /// the unified scroll system.
3224    ///
3225    /// This is called after `layout_and_generate_display_list()` to ensure that
3226    /// text cursors remain visible after text input or cursor movement.
3227    ///
3228    /// Algorithm:
3229    /// 1. Get the focused cursor rect (if any)
3230    /// 2. Find the scrollable ancestor container
3231    /// 3. Calculate scroll delta to bring cursor into view
3232    /// 4. Apply instant scroll (no animation for text input responsiveness)
3233    fn scroll_focused_cursor_into_view(&mut self) {
3234        // Redirect to unified scroll system
3235        self.scroll_selection_into_view(SelectionScrollType::Cursor, ScrollMode::Instant);
3236    }
3237}
3238
3239/// Type of selection bounds to scroll into view
3240#[derive(Debug, Clone, Copy)]
3241pub enum SelectionScrollType {
3242    /// Scroll cursor (0-size selection) into view
3243    Cursor,
3244    /// Scroll current selection bounds into view
3245    Selection,
3246    /// Scroll for drag selection (use mouse position for direction/speed)
3247    DragSelection { mouse_position: LogicalPosition },
3248}
3249
3250/// Scroll animation mode
3251#[derive(Debug, Clone, Copy)]
3252pub enum ScrollMode {
3253    /// Instant scroll with fixed padding (for typing, arrow keys)
3254    Instant,
3255    /// Accelerated scroll based on distance from edge (for drag-to-scroll)
3256    Accelerated,
3257}
3258
3259/// Distance from rect edges to container edges (for acceleration calculation)
3260#[derive(Debug, Clone, Copy)]
3261struct EdgeDistance {
3262    left: f32,
3263    right: f32,
3264    top: f32,
3265    bottom: f32,
3266}
3267
3268/// Calculate distance from rect to container edges
3269fn calculate_edge_distance(rect: LogicalRect, container: LogicalRect) -> EdgeDistance {
3270    EdgeDistance {
3271        // Distance from rect's left edge to container's left edge
3272        left: (rect.origin.x - container.origin.x).max(0.0),
3273        // Distance from container's right edge to rect's right edge
3274        right: ((container.origin.x + container.size.width) - (rect.origin.x + rect.size.width))
3275            .max(0.0),
3276        // Distance from rect's top edge to container's top edge
3277        top: (rect.origin.y - container.origin.y).max(0.0),
3278        // Distance from container's bottom edge to rect's bottom edge
3279        bottom: ((container.origin.y + container.size.height) - (rect.origin.y + rect.size.height))
3280            .max(0.0),
3281    }
3282}
3283
3284/// Calculate scroll delta with fixed padding (instant scroll mode)
3285fn calculate_instant_scroll_delta(
3286    bounds: LogicalRect,
3287    visible_area: LogicalRect,
3288) -> LogicalPosition {
3289    const PADDING: f32 = 5.0;
3290    let mut delta = LogicalPosition::zero();
3291
3292    // Horizontal scrolling
3293    if bounds.origin.x < visible_area.origin.x + PADDING {
3294        delta.x = bounds.origin.x - visible_area.origin.x - PADDING;
3295    } else if bounds.origin.x + bounds.size.width
3296        > visible_area.origin.x + visible_area.size.width - PADDING
3297    {
3298        delta.x = (bounds.origin.x + bounds.size.width)
3299            - (visible_area.origin.x + visible_area.size.width)
3300            + PADDING;
3301    }
3302
3303    // Vertical scrolling
3304    if bounds.origin.y < visible_area.origin.y + PADDING {
3305        delta.y = bounds.origin.y - visible_area.origin.y - PADDING;
3306    } else if bounds.origin.y + bounds.size.height
3307        > visible_area.origin.y + visible_area.size.height - PADDING
3308    {
3309        delta.y = (bounds.origin.y + bounds.size.height)
3310            - (visible_area.origin.y + visible_area.size.height)
3311            + PADDING;
3312    }
3313
3314    delta
3315}
3316
3317/// Calculate scroll delta with distance-based acceleration (drag-to-scroll mode)
3318fn calculate_accelerated_scroll_delta(distance: EdgeDistance) -> LogicalPosition {
3319    // Acceleration zones (in pixels from edge)
3320    const DEAD_ZONE: f32 = 20.0;
3321    const SLOW_ZONE: f32 = 50.0;
3322    const MEDIUM_ZONE: f32 = 100.0;
3323    const FAST_ZONE: f32 = 200.0;
3324
3325    // Scroll speeds (pixels per frame at 60fps)
3326    const SLOW_SPEED: f32 = 2.0;
3327    const MEDIUM_SPEED: f32 = 4.0;
3328    const FAST_SPEED: f32 = 8.0;
3329    const VERY_FAST_SPEED: f32 = 16.0;
3330
3331    // Helper to calculate speed for one direction
3332    let speed_for_distance = |dist: f32| -> f32 {
3333        if dist < DEAD_ZONE {
3334            0.0
3335        } else if dist < SLOW_ZONE {
3336            SLOW_SPEED
3337        } else if dist < MEDIUM_ZONE {
3338            MEDIUM_SPEED
3339        } else if dist < FAST_ZONE {
3340            FAST_SPEED
3341        } else {
3342            VERY_FAST_SPEED
3343        }
3344    };
3345
3346    // Calculate horizontal scroll (left vs right)
3347    let scroll_x = if distance.left < distance.right {
3348        // Closer to left edge - scroll left
3349        -speed_for_distance(distance.left)
3350    } else {
3351        // Closer to right edge - scroll right
3352        speed_for_distance(distance.right)
3353    };
3354
3355    // Calculate vertical scroll (top vs bottom)
3356    let scroll_y = if distance.top < distance.bottom {
3357        // Closer to top edge - scroll up
3358        -speed_for_distance(distance.top)
3359    } else {
3360        // Closer to bottom edge - scroll down
3361        speed_for_distance(distance.bottom)
3362    };
3363
3364    LogicalPosition::new(scroll_x, scroll_y)
3365}
3366
3367/// Result of a layout operation
3368pub struct LayoutResult {
3369    pub display_list: DisplayList,
3370    pub warnings: Vec<String>,
3371}
3372
3373impl LayoutResult {
3374    pub fn new(display_list: DisplayList, warnings: Vec<String>) -> Self {
3375        Self {
3376            display_list,
3377            warnings,
3378        }
3379    }
3380}
3381
3382impl LayoutWindow {
3383    /// Runs a single timer, similar to CallbacksOfHitTest.call()
3384    ///
3385    /// NOTE: The timer has to be selected first by the calling code and verified
3386    /// that it is ready to run
3387    #[cfg(feature = "std")]
3388    pub fn run_single_timer(
3389        &mut self,
3390        timer_id: usize,
3391        frame_start: Instant,
3392        current_window_handle: &RawWindowHandle,
3393        gl_context: &OptionGlContextPtr,
3394        image_cache: &mut ImageCache,
3395        system_fonts: &mut FcFontCache,
3396        system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3397        system_callbacks: &ExternalSystemCallbacks,
3398        previous_window_state: &Option<FullWindowState>,
3399        current_window_state: &FullWindowState,
3400        renderer_resources: &RendererResources,
3401    ) -> CallCallbacksResult {
3402        use std::collections::BTreeMap;
3403
3404        use crate::callbacks::{CallCallbacksResult, CallbackInfo};
3405
3406        let mut ret = CallCallbacksResult {
3407            should_scroll_render: false,
3408            callbacks_update_screen: Update::DoNothing,
3409            modified_window_state: None,
3410            css_properties_changed: None,
3411            words_changed: None,
3412            images_changed: None,
3413            image_masks_changed: None,
3414            image_callbacks_changed: None,
3415            nodes_scrolled_in_callbacks: None,
3416            update_focused_node: FocusUpdateRequest::NoChange,
3417            timers: None,
3418            threads: None,
3419            timers_removed: None,
3420            threads_removed: None,
3421            windows_created: Vec::new(),
3422            menus_to_open: Vec::new(),
3423            tooltips_to_show: Vec::new(),
3424            hide_tooltip: false,
3425            cursor_changed: false,
3426            stop_propagation: false,
3427            stop_immediate_propagation: false,
3428            prevent_default: false,
3429            hit_test_update_requested: None,
3430            queued_window_states: Vec::new(),
3431            text_input_triggered: Vec::new(),
3432            begin_interactive_move: false,
3433        };
3434
3435        let mut should_terminate = TerminateTimer::Continue;
3436
3437        let current_scroll_states_nested = self.get_nested_scroll_states(DomId::ROOT_ID);
3438
3439        // Check if timer exists and get node_id before borrowing self mutably
3440        let timer_exists = self.timers.contains_key(&TimerId { id: timer_id });
3441        let timer_node_id = self
3442            .timers
3443            .get(&TimerId { id: timer_id })
3444            .and_then(|t| t.node_id.into_option());
3445
3446        if timer_exists {
3447            // TODO: store the hit DOM of the timer?
3448            let hit_dom_node = match timer_node_id {
3449                Some(s) => s,
3450                None => DomNodeId {
3451                    dom: DomId::ROOT_ID,
3452                    node: NodeHierarchyItemId::from_crate_internal(None),
3453                },
3454            };
3455            let cursor_relative_to_item = OptionLogicalPosition::None;
3456            let cursor_in_viewport = OptionLogicalPosition::None;
3457
3458            // Create changes container for callback transaction system
3459            // Uses Arc<Mutex> so that cloned CallbackInfo (e.g., in timer callbacks)
3460            // still push to the same collection
3461            let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3462
3463            // Create reference data container (syntax sugar to reduce parameter count)
3464            // First get the ctx from the timer's callback before we borrow timer again
3465            let timer_ctx = self
3466                .timers
3467                .get(&TimerId { id: timer_id })
3468                .map(|t| t.callback.ctx.clone())
3469                .unwrap_or(OptionRefAny::None);
3470
3471            let ref_data = crate::callbacks::CallbackInfoRefData {
3472                layout_window: self,
3473                renderer_resources,
3474                previous_window_state,
3475                current_window_state,
3476                gl_context,
3477                current_scroll_manager: &current_scroll_states_nested,
3478                current_window_handle,
3479                system_callbacks,
3480                system_style,
3481                monitors: self.monitors.clone(),
3482                #[cfg(feature = "icu")]
3483                icu_localizer: self.icu_localizer.clone(),
3484                ctx: timer_ctx,
3485            };
3486
3487            let callback_info = CallbackInfo::new(
3488                &ref_data,
3489                &callback_changes,
3490                hit_dom_node,
3491                cursor_relative_to_item,
3492                cursor_in_viewport,
3493            ); // Now we can borrow the timer mutably
3494            let timer = self.timers.get_mut(&TimerId { id: timer_id }).unwrap();
3495            let tcr = timer.invoke(&callback_info, &system_callbacks.get_system_time_fn);
3496
3497            ret.callbacks_update_screen = tcr.should_update;
3498            should_terminate = tcr.should_terminate;
3499
3500            // Extract changes from the Arc<Mutex> - they may have been pushed by
3501            // cloned CallbackInfo instances (e.g., in timer callbacks)
3502            let collected_changes = callback_changes
3503                .lock()
3504                .map(|mut guard| core::mem::take(&mut *guard))
3505                .unwrap_or_default();
3506
3507            // Apply callback changes collected during timer execution
3508            let change_result = self.apply_callback_changes(
3509                collected_changes,
3510                current_window_state,
3511                image_cache,
3512                system_fonts,
3513            );
3514
3515            // Queue IFrame updates for next frame
3516            if !change_result.iframes_to_update.is_empty() {
3517                self.queue_iframe_updates(change_result.iframes_to_update.clone());
3518            }
3519
3520            // Transfer results from CallbackChangeResult to CallCallbacksResult
3521            ret.stop_propagation = change_result.stop_propagation;
3522            ret.prevent_default = change_result.prevent_default;
3523            ret.tooltips_to_show = change_result.tooltips_to_show;
3524            ret.hide_tooltip = change_result.hide_tooltip;
3525
3526            if !change_result.timers.is_empty() {
3527                ret.timers = Some(change_result.timers);
3528            }
3529            if !change_result.threads.is_empty() {
3530                ret.threads = Some(change_result.threads);
3531            }
3532            if change_result.modified_window_state != *current_window_state {
3533                ret.modified_window_state = Some(change_result.modified_window_state);
3534            }
3535            if !change_result.threads_removed.is_empty() {
3536                ret.threads_removed = Some(change_result.threads_removed);
3537            }
3538            if !change_result.timers_removed.is_empty() {
3539                ret.timers_removed = Some(change_result.timers_removed);
3540            }
3541            if !change_result.words_changed.is_empty() {
3542                ret.words_changed = Some(change_result.words_changed);
3543            }
3544            if !change_result.images_changed.is_empty() {
3545                ret.images_changed = Some(change_result.images_changed);
3546            }
3547            if !change_result.image_masks_changed.is_empty() {
3548                ret.image_masks_changed = Some(change_result.image_masks_changed);
3549            }
3550            if !change_result.css_properties_changed.is_empty() {
3551                ret.css_properties_changed = Some(change_result.css_properties_changed);
3552            }
3553            if !change_result.image_callbacks_changed.is_empty() {
3554                ret.image_callbacks_changed = Some(change_result.image_callbacks_changed);
3555            }
3556            if !change_result.nodes_scrolled.is_empty() {
3557                ret.nodes_scrolled_in_callbacks = Some(change_result.nodes_scrolled);
3558            }
3559
3560            // Forward hit test update request to shell layer
3561            if change_result.hit_test_update_requested.is_some() {
3562                ret.hit_test_update_requested = change_result.hit_test_update_requested;
3563            }
3564
3565            // Forward queued window states to shell layer for sequential processing
3566            if !change_result.queued_window_states.is_empty() {
3567                ret.queued_window_states = change_result.queued_window_states;
3568            }
3569
3570            // Forward text_input_triggered to shell layer for recursive callback processing
3571            if !change_result.text_input_triggered.is_empty() {
3572                ret.text_input_triggered = change_result.text_input_triggered;
3573            }
3574
3575            // Forward begin_interactive_move to shell layer (Wayland xdg_toplevel_move)
3576            if change_result.begin_interactive_move {
3577                ret.begin_interactive_move = true;
3578            }
3579
3580            // Handle focus target outside the timer block so it's available later
3581            if let Some(ft) = change_result.focus_target {
3582                if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
3583                    &ft,
3584                    &self.layout_results,
3585                    self.focus_manager.get_focused_node().copied(),
3586                ) {
3587                    ret.update_focused_node = match new_focus_node {
3588                        Some(node) => FocusUpdateRequest::FocusNode(node),
3589                        None => FocusUpdateRequest::ClearFocus,
3590                    };
3591                }
3592            }
3593        }
3594
3595        if should_terminate == TerminateTimer::Terminate {
3596            ret.timers_removed
3597                .get_or_insert_with(|| std::collections::BTreeSet::new())
3598                .insert(TimerId { id: timer_id });
3599        }
3600
3601        return ret;
3602    }
3603
3604    #[cfg(feature = "std")]
3605    pub fn run_all_threads(
3606        &mut self,
3607        data: &mut RefAny,
3608        current_window_handle: &RawWindowHandle,
3609        gl_context: &OptionGlContextPtr,
3610        image_cache: &mut ImageCache,
3611        system_fonts: &mut FcFontCache,
3612        system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3613        system_callbacks: &ExternalSystemCallbacks,
3614        previous_window_state: &Option<FullWindowState>,
3615        current_window_state: &FullWindowState,
3616        renderer_resources: &RendererResources,
3617    ) -> CallCallbacksResult {
3618        use std::collections::BTreeSet;
3619
3620        use crate::{
3621            callbacks::{CallCallbacksResult, CallbackInfo},
3622            thread::{OptionThreadReceiveMsg, ThreadReceiveMsg, ThreadWriteBackMsg},
3623        };
3624
3625        let mut ret = CallCallbacksResult {
3626            should_scroll_render: false,
3627            callbacks_update_screen: Update::DoNothing,
3628            modified_window_state: None,
3629            css_properties_changed: None,
3630            words_changed: None,
3631            images_changed: None,
3632            image_masks_changed: None,
3633            image_callbacks_changed: None,
3634            nodes_scrolled_in_callbacks: None,
3635            update_focused_node: FocusUpdateRequest::NoChange,
3636            timers: None,
3637            threads: None,
3638            timers_removed: None,
3639            threads_removed: None,
3640            windows_created: Vec::new(),
3641            menus_to_open: Vec::new(),
3642            tooltips_to_show: Vec::new(),
3643            hide_tooltip: false,
3644            cursor_changed: false,
3645            stop_propagation: false,
3646            stop_immediate_propagation: false,
3647            prevent_default: false,
3648            hit_test_update_requested: None,
3649            queued_window_states: Vec::new(),
3650            text_input_triggered: Vec::new(),
3651            begin_interactive_move: false,
3652        };
3653
3654        let mut ret_modified_window_state = current_window_state.clone();
3655        let ret_window_state = ret_modified_window_state.clone();
3656        let mut ret_timers = FastHashMap::new();
3657        let mut ret_timers_removed = FastBTreeSet::new();
3658        let mut ret_threads = FastHashMap::new();
3659        let mut ret_threads_removed = FastBTreeSet::new();
3660        let mut ret_words_changed = BTreeMap::new();
3661        let mut ret_images_changed = BTreeMap::new();
3662        let mut ret_image_masks_changed = BTreeMap::new();
3663        let mut ret_css_properties_changed = BTreeMap::new();
3664        let mut ret_nodes_scrolled_in_callbacks = BTreeMap::new();
3665        let mut new_focus_target = None;
3666        let mut stop_propagation = false;
3667        let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
3668
3669        // Collect thread IDs first to avoid borrowing self.threads while accessing self
3670        let thread_ids: Vec<ThreadId> = self.threads.keys().copied().collect();
3671
3672        for thread_id in thread_ids {
3673            let thread = match self.threads.get_mut(&thread_id) {
3674                Some(t) => t,
3675                None => continue,
3676            };
3677
3678            let hit_dom_node = DomNodeId {
3679                dom: DomId::ROOT_ID,
3680                node: NodeHierarchyItemId::from_crate_internal(None),
3681            };
3682            let cursor_relative_to_item = OptionLogicalPosition::None;
3683            let cursor_in_viewport = OptionLogicalPosition::None;
3684
3685            // Lock the mutex, extract data, then drop the guard before creating CallbackInfo
3686            let (msg, writeback_data_ptr, is_finished) = {
3687                let thread_inner = &mut *match thread.ptr.lock().ok() {
3688                    Some(s) => s,
3689                    None => {
3690                        ret.threads_removed
3691                            .get_or_insert_with(|| BTreeSet::default())
3692                            .insert(thread_id);
3693                        continue;
3694                    }
3695                };
3696
3697                let _ = thread_inner.sender_send(ThreadSendMsg::Tick);
3698                let update = thread_inner.receiver_try_recv();
3699                let msg = match update {
3700                    OptionThreadReceiveMsg::None => continue,
3701                    OptionThreadReceiveMsg::Some(s) => s,
3702                };
3703
3704                let writeback_data_ptr: *mut RefAny = &mut thread_inner.writeback_data as *mut _;
3705                let is_finished = thread_inner.is_finished();
3706
3707                (msg, writeback_data_ptr, is_finished)
3708                // MutexGuard is dropped here
3709            };
3710
3711            let ThreadWriteBackMsg {
3712                refany: mut data,
3713                callback,
3714            } = match msg {
3715                ThreadReceiveMsg::Update(update_screen) => {
3716                    ret.callbacks_update_screen.max_self(update_screen);
3717                    continue;
3718                }
3719                ThreadReceiveMsg::WriteBack(t) => t,
3720            };
3721
3722            // Create changes container for callback transaction system
3723            let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3724
3725            // Create reference data container (syntax sugar to reduce parameter count)
3726            let ref_data = crate::callbacks::CallbackInfoRefData {
3727                layout_window: self,
3728                renderer_resources,
3729                previous_window_state,
3730                current_window_state,
3731                gl_context,
3732                current_scroll_manager: &current_scroll_states,
3733                current_window_handle,
3734                system_callbacks,
3735                system_style: system_style.clone(),
3736                monitors: self.monitors.clone(),
3737                #[cfg(feature = "icu")]
3738                icu_localizer: self.icu_localizer.clone(),
3739                ctx: callback.ctx.clone(),
3740            };
3741
3742            let callback_info = CallbackInfo::new(
3743                &ref_data,
3744                &callback_changes,
3745                hit_dom_node,
3746                cursor_relative_to_item,
3747                cursor_in_viewport,
3748            );
3749            let callback_update = (callback.cb)(
3750                unsafe { (*writeback_data_ptr).clone() },
3751                data.clone(),
3752                callback_info,
3753            );
3754            ret.callbacks_update_screen.max_self(callback_update);
3755
3756            // Extract changes from the Arc<Mutex>
3757            let collected_changes = callback_changes
3758                .lock()
3759                .map(|mut guard| core::mem::take(&mut *guard))
3760                .unwrap_or_default();
3761
3762            // Apply callback changes collected during thread writeback
3763            let change_result = self.apply_callback_changes(
3764                collected_changes,
3765                current_window_state,
3766                image_cache,
3767                system_fonts,
3768            );
3769
3770            // Queue any IFrame updates from this callback
3771            self.queue_iframe_updates(change_result.iframes_to_update);
3772
3773            ret.stop_propagation = ret.stop_propagation || change_result.stop_propagation;
3774            ret.prevent_default = ret.prevent_default || change_result.prevent_default;
3775            ret.tooltips_to_show.extend(change_result.tooltips_to_show);
3776            ret.hide_tooltip = ret.hide_tooltip || change_result.hide_tooltip;
3777            ret.begin_interactive_move = ret.begin_interactive_move || change_result.begin_interactive_move;
3778
3779            // Forward hit test update request
3780            if change_result.hit_test_update_requested.is_some() {
3781                ret.hit_test_update_requested = change_result.hit_test_update_requested;
3782            }
3783
3784            // Merge changes into accumulated results
3785            ret_timers.extend(change_result.timers);
3786            ret_threads.extend(change_result.threads);
3787            ret_timers_removed.extend(change_result.timers_removed);
3788            ret_threads_removed.extend(change_result.threads_removed);
3789
3790            for (dom_id, nodes) in change_result.words_changed {
3791                ret_words_changed
3792                    .entry(dom_id)
3793                    .or_insert_with(BTreeMap::new)
3794                    .extend(nodes);
3795            }
3796            for (dom_id, nodes) in change_result.images_changed {
3797                ret_images_changed
3798                    .entry(dom_id)
3799                    .or_insert_with(BTreeMap::new)
3800                    .extend(nodes);
3801            }
3802            for (dom_id, nodes) in change_result.image_masks_changed {
3803                ret_image_masks_changed
3804                    .entry(dom_id)
3805                    .or_insert_with(BTreeMap::new)
3806                    .extend(nodes);
3807            }
3808            for (dom_id, nodes) in change_result.css_properties_changed {
3809                ret_css_properties_changed
3810                    .entry(dom_id)
3811                    .or_insert_with(BTreeMap::new)
3812                    .extend(nodes);
3813            }
3814            for (dom_id, nodes) in change_result.nodes_scrolled {
3815                ret_nodes_scrolled_in_callbacks
3816                    .entry(dom_id)
3817                    .or_insert_with(BTreeMap::new)
3818                    .extend(nodes);
3819            }
3820
3821            if change_result.modified_window_state != *current_window_state {
3822                ret_modified_window_state = change_result.modified_window_state;
3823            }
3824
3825            if let Some(ft) = change_result.focus_target {
3826                new_focus_target = Some(ft);
3827            }
3828
3829            if is_finished {
3830                ret.threads_removed
3831                    .get_or_insert_with(|| BTreeSet::default())
3832                    .insert(thread_id);
3833            }
3834        }
3835
3836        if !ret_timers.is_empty() {
3837            ret.timers = Some(ret_timers);
3838        }
3839        if !ret_threads.is_empty() {
3840            ret.threads = Some(ret_threads);
3841        }
3842        if ret_modified_window_state != ret_window_state {
3843            ret.modified_window_state = Some(ret_modified_window_state);
3844        }
3845        if !ret_threads_removed.is_empty() {
3846            ret.threads_removed = Some(ret_threads_removed);
3847        }
3848        if !ret_timers_removed.is_empty() {
3849            ret.timers_removed = Some(ret_timers_removed);
3850        }
3851        if !ret_words_changed.is_empty() {
3852            ret.words_changed = Some(ret_words_changed);
3853        }
3854        if !ret_images_changed.is_empty() {
3855            ret.images_changed = Some(ret_images_changed);
3856        }
3857        if !ret_image_masks_changed.is_empty() {
3858            ret.image_masks_changed = Some(ret_image_masks_changed);
3859        }
3860        if !ret_css_properties_changed.is_empty() {
3861            ret.css_properties_changed = Some(ret_css_properties_changed);
3862        }
3863        if !ret_nodes_scrolled_in_callbacks.is_empty() {
3864            ret.nodes_scrolled_in_callbacks = Some(ret_nodes_scrolled_in_callbacks);
3865        }
3866
3867        if let Some(ft) = new_focus_target {
3868            if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
3869                &ft,
3870                &self.layout_results,
3871                self.focus_manager.get_focused_node().copied(),
3872            ) {
3873                ret.update_focused_node = match new_focus_node {
3874                    Some(node) => FocusUpdateRequest::FocusNode(node),
3875                    None => FocusUpdateRequest::ClearFocus,
3876                };
3877            }
3878        }
3879
3880        return ret;
3881    }
3882
3883    /// Invokes a single callback (used for on_window_create, on_window_shutdown, etc.)
3884    pub fn invoke_single_callback(
3885        &mut self,
3886        callback: &mut Callback,
3887        data: &mut RefAny,
3888        current_window_handle: &RawWindowHandle,
3889        gl_context: &OptionGlContextPtr,
3890        image_cache: &mut ImageCache,
3891        system_fonts: &mut FcFontCache,
3892        system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3893        system_callbacks: &ExternalSystemCallbacks,
3894        previous_window_state: &Option<FullWindowState>,
3895        current_window_state: &FullWindowState,
3896        renderer_resources: &RendererResources,
3897    ) -> CallCallbacksResult {
3898        use crate::callbacks::{CallCallbacksResult, Callback, CallbackInfo};
3899
3900        let hit_dom_node = DomNodeId {
3901            dom: DomId::ROOT_ID,
3902            node: NodeHierarchyItemId::from_crate_internal(None),
3903        };
3904
3905        let mut ret = CallCallbacksResult {
3906            should_scroll_render: false,
3907            callbacks_update_screen: Update::DoNothing,
3908            modified_window_state: None,
3909            css_properties_changed: None,
3910            words_changed: None,
3911            images_changed: None,
3912            image_masks_changed: None,
3913            image_callbacks_changed: None,
3914            nodes_scrolled_in_callbacks: None,
3915            update_focused_node: FocusUpdateRequest::NoChange,
3916            timers: None,
3917            threads: None,
3918            timers_removed: None,
3919            threads_removed: None,
3920            windows_created: Vec::new(),
3921            menus_to_open: Vec::new(),
3922            tooltips_to_show: Vec::new(),
3923            hide_tooltip: false,
3924            cursor_changed: false,
3925            stop_propagation: false,
3926            stop_immediate_propagation: false,
3927            prevent_default: false,
3928            hit_test_update_requested: None,
3929            queued_window_states: Vec::new(),
3930            text_input_triggered: Vec::new(),
3931            begin_interactive_move: false,
3932        };
3933
3934        let mut ret_modified_window_state = current_window_state.clone();
3935        let ret_window_state = ret_modified_window_state.clone();
3936        let mut ret_timers = FastHashMap::new();
3937        let mut ret_timers_removed = FastBTreeSet::new();
3938        let mut ret_threads = FastHashMap::new();
3939        let mut ret_threads_removed = FastBTreeSet::new();
3940        let mut ret_words_changed = BTreeMap::new();
3941        let mut ret_images_changed = BTreeMap::new();
3942        let mut ret_image_masks_changed = BTreeMap::new();
3943        let mut ret_css_properties_changed = BTreeMap::new();
3944        let mut ret_nodes_scrolled_in_callbacks = BTreeMap::new();
3945        let mut new_focus_target = None;
3946        let mut stop_propagation = false;
3947        let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
3948
3949        let cursor_relative_to_item = OptionLogicalPosition::None;
3950        let cursor_in_viewport = match current_window_state.mouse_state.cursor_position.get_position() {
3951            Some(pos) => OptionLogicalPosition::Some(pos),
3952            None => OptionLogicalPosition::None,
3953        };
3954
3955        // Create changes container for callback transaction system
3956        let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3957
3958        // Create reference data container (syntax sugar to reduce parameter count)
3959        let ref_data = crate::callbacks::CallbackInfoRefData {
3960            layout_window: self,
3961            renderer_resources,
3962            previous_window_state,
3963            current_window_state,
3964            gl_context,
3965            current_scroll_manager: &current_scroll_states,
3966            current_window_handle,
3967            system_callbacks,
3968            system_style,
3969            monitors: self.monitors.clone(),
3970            #[cfg(feature = "icu")]
3971            icu_localizer: self.icu_localizer.clone(),
3972            ctx: OptionRefAny::None,
3973        };
3974
3975        let callback_info = CallbackInfo::new(
3976            &ref_data,
3977            &callback_changes,
3978            hit_dom_node,
3979            cursor_relative_to_item,
3980            cursor_in_viewport,
3981        );
3982
3983        ret.callbacks_update_screen = (callback.cb)(data.clone(), callback_info);
3984
3985        // Extract changes from the Arc<Mutex>
3986        let collected_changes = callback_changes
3987            .lock()
3988            .map(|mut guard| core::mem::take(&mut *guard))
3989            .unwrap_or_default();
3990
3991        // Apply callback changes collected during callback execution
3992        let change_result = self.apply_callback_changes(
3993            collected_changes,
3994            current_window_state,
3995            image_cache,
3996            system_fonts,
3997        );
3998
3999        // Queue any IFrame updates from this callback
4000        self.queue_iframe_updates(change_result.iframes_to_update);
4001
4002        ret.stop_propagation = change_result.stop_propagation;
4003        ret.prevent_default = change_result.prevent_default;
4004        ret.tooltips_to_show = change_result.tooltips_to_show;
4005        ret.hide_tooltip = change_result.hide_tooltip;
4006        ret.begin_interactive_move = ret.begin_interactive_move || change_result.begin_interactive_move;
4007
4008        // Forward hit test update request (invoke_single_callback)
4009        if change_result.hit_test_update_requested.is_some() {
4010            ret.hit_test_update_requested = change_result.hit_test_update_requested;
4011        }
4012
4013        ret_timers.extend(change_result.timers);
4014        ret_threads.extend(change_result.threads);
4015        ret_timers_removed.extend(change_result.timers_removed);
4016        ret_threads_removed.extend(change_result.threads_removed);
4017        ret_words_changed.extend(change_result.words_changed);
4018        ret_images_changed.extend(change_result.images_changed);
4019        ret_image_masks_changed.extend(change_result.image_masks_changed);
4020        ret_css_properties_changed.extend(change_result.css_properties_changed);
4021        ret_nodes_scrolled_in_callbacks.append(&mut change_result.nodes_scrolled.clone());
4022
4023        if change_result.modified_window_state != *current_window_state {
4024            ret_modified_window_state = change_result.modified_window_state;
4025        }
4026
4027        new_focus_target = change_result.focus_target.or(new_focus_target);
4028
4029        if !ret_timers.is_empty() {
4030            ret.timers = Some(ret_timers);
4031        }
4032        if !ret_threads.is_empty() {
4033            ret.threads = Some(ret_threads);
4034        }
4035        if ret_modified_window_state != ret_window_state {
4036            ret.modified_window_state = Some(ret_modified_window_state);
4037        }
4038        if !ret_threads_removed.is_empty() {
4039            ret.threads_removed = Some(ret_threads_removed);
4040        }
4041        if !ret_timers_removed.is_empty() {
4042            ret.timers_removed = Some(ret_timers_removed);
4043        }
4044        if !ret_words_changed.is_empty() {
4045            ret.words_changed = Some(ret_words_changed);
4046        }
4047        if !ret_images_changed.is_empty() {
4048            ret.images_changed = Some(ret_images_changed);
4049        }
4050        if !ret_image_masks_changed.is_empty() {
4051            ret.image_masks_changed = Some(ret_image_masks_changed);
4052        }
4053        if !ret_css_properties_changed.is_empty() {
4054            ret.css_properties_changed = Some(ret_css_properties_changed);
4055        }
4056        if !ret_nodes_scrolled_in_callbacks.is_empty() {
4057            ret.nodes_scrolled_in_callbacks = Some(ret_nodes_scrolled_in_callbacks);
4058        }
4059
4060        if let Some(ft) = new_focus_target {
4061            if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
4062                &ft,
4063                &self.layout_results,
4064                self.focus_manager.get_focused_node().copied(),
4065            ) {
4066                ret.update_focused_node = match new_focus_node {
4067                    Some(node) => FocusUpdateRequest::FocusNode(node),
4068                    None => FocusUpdateRequest::ClearFocus,
4069                };
4070            }
4071        }
4072
4073        return ret;
4074    }
4075
4076    /// Invokes a menu callback
4077    pub fn invoke_menu_callback(
4078        &mut self,
4079        menu_callback: &mut MenuCallback,
4080        hit_dom_node: DomNodeId,
4081        current_window_handle: &RawWindowHandle,
4082        gl_context: &OptionGlContextPtr,
4083        image_cache: &mut ImageCache,
4084        system_fonts: &mut FcFontCache,
4085        system_style: std::sync::Arc<azul_css::system::SystemStyle>,
4086        system_callbacks: &ExternalSystemCallbacks,
4087        previous_window_state: &Option<FullWindowState>,
4088        current_window_state: &FullWindowState,
4089        renderer_resources: &RendererResources,
4090    ) -> CallCallbacksResult {
4091        use crate::callbacks::{CallCallbacksResult, CallbackInfo, MenuCallback};
4092
4093        let mut ret = CallCallbacksResult {
4094            should_scroll_render: false,
4095            callbacks_update_screen: Update::DoNothing,
4096            modified_window_state: None,
4097            css_properties_changed: None,
4098            words_changed: None,
4099            images_changed: None,
4100            image_masks_changed: None,
4101            image_callbacks_changed: None,
4102            nodes_scrolled_in_callbacks: None,
4103            update_focused_node: FocusUpdateRequest::NoChange,
4104            timers: None,
4105            threads: None,
4106            timers_removed: None,
4107            threads_removed: None,
4108            windows_created: Vec::new(),
4109            menus_to_open: Vec::new(),
4110            tooltips_to_show: Vec::new(),
4111            hide_tooltip: false,
4112            cursor_changed: false,
4113            stop_propagation: false,
4114            stop_immediate_propagation: false,
4115            prevent_default: false,
4116            hit_test_update_requested: None,
4117            queued_window_states: Vec::new(),
4118            text_input_triggered: Vec::new(),
4119            begin_interactive_move: false,
4120        };
4121
4122        let mut ret_modified_window_state = current_window_state.clone();
4123        let ret_window_state = ret_modified_window_state.clone();
4124        let mut ret_timers = FastHashMap::new();
4125        let mut ret_timers_removed = FastBTreeSet::new();
4126        let mut ret_threads = FastHashMap::new();
4127        let mut ret_threads_removed = FastBTreeSet::new();
4128        let mut ret_words_changed = BTreeMap::new();
4129        let mut ret_images_changed = BTreeMap::new();
4130        let mut ret_image_masks_changed = BTreeMap::new();
4131        let mut ret_css_properties_changed = BTreeMap::new();
4132        let mut ret_nodes_scrolled_in_callbacks = BTreeMap::new();
4133        let mut new_focus_target = None;
4134        let mut stop_propagation = false;
4135        let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
4136
4137        let cursor_relative_to_item = OptionLogicalPosition::None;
4138        let cursor_in_viewport = match current_window_state.mouse_state.cursor_position.get_position() {
4139            Some(pos) => OptionLogicalPosition::Some(pos),
4140            None => OptionLogicalPosition::None,
4141        };
4142
4143        // Create changes container for callback transaction system
4144        let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
4145
4146        // Create reference data container (syntax sugar to reduce parameter count)
4147        let ref_data = crate::callbacks::CallbackInfoRefData {
4148            layout_window: self,
4149            renderer_resources,
4150            previous_window_state,
4151            current_window_state,
4152            gl_context,
4153            current_scroll_manager: &current_scroll_states,
4154            current_window_handle,
4155            system_callbacks,
4156            system_style,
4157            monitors: self.monitors.clone(),
4158            #[cfg(feature = "icu")]
4159            icu_localizer: self.icu_localizer.clone(),
4160            ctx: OptionRefAny::None,
4161        };
4162
4163        let callback_info = CallbackInfo::new(
4164            &ref_data,
4165            &callback_changes,
4166            hit_dom_node,
4167            cursor_relative_to_item,
4168            cursor_in_viewport,
4169        );
4170
4171        ret.callbacks_update_screen =
4172            (menu_callback.callback.cb)(menu_callback.refany.clone(), callback_info);
4173
4174        // Extract changes from the Arc<Mutex>
4175        let collected_changes = callback_changes
4176            .lock()
4177            .map(|mut guard| core::mem::take(&mut *guard))
4178            .unwrap_or_default();
4179
4180        // Apply callback changes collected during menu callback execution
4181        let change_result = self.apply_callback_changes(
4182            collected_changes,
4183            current_window_state,
4184            image_cache,
4185            system_fonts,
4186        );
4187
4188        // Queue any IFrame updates from this callback
4189        self.queue_iframe_updates(change_result.iframes_to_update);
4190
4191        ret.stop_propagation = change_result.stop_propagation;
4192        ret.prevent_default = change_result.prevent_default;
4193        ret.tooltips_to_show = change_result.tooltips_to_show;
4194        ret.hide_tooltip = change_result.hide_tooltip;
4195        ret.begin_interactive_move = ret.begin_interactive_move || change_result.begin_interactive_move;
4196
4197        // Forward hit test update request (invoke_menu_callback)
4198        if change_result.hit_test_update_requested.is_some() {
4199            ret.hit_test_update_requested = change_result.hit_test_update_requested;
4200        }
4201
4202        ret_timers.extend(change_result.timers);
4203        ret_threads.extend(change_result.threads);
4204        ret_timers_removed.extend(change_result.timers_removed);
4205        ret_threads_removed.extend(change_result.threads_removed);
4206        ret_words_changed.extend(change_result.words_changed);
4207        ret_images_changed.extend(change_result.images_changed);
4208        ret_image_masks_changed.extend(change_result.image_masks_changed);
4209        ret_css_properties_changed.extend(change_result.css_properties_changed);
4210        ret_nodes_scrolled_in_callbacks.append(&mut change_result.nodes_scrolled.clone());
4211
4212        if change_result.modified_window_state != *current_window_state {
4213            ret_modified_window_state = change_result.modified_window_state;
4214        }
4215
4216        new_focus_target = change_result.focus_target.or(new_focus_target);
4217
4218        if !ret_timers.is_empty() {
4219            ret.timers = Some(ret_timers);
4220        }
4221        if !ret_threads.is_empty() {
4222            ret.threads = Some(ret_threads);
4223        }
4224        if ret_modified_window_state != ret_window_state {
4225            ret.modified_window_state = Some(ret_modified_window_state);
4226        }
4227        if !ret_threads_removed.is_empty() {
4228            ret.threads_removed = Some(ret_threads_removed);
4229        }
4230        if !ret_timers_removed.is_empty() {
4231            ret.timers_removed = Some(ret_timers_removed);
4232        }
4233        if !ret_words_changed.is_empty() {
4234            ret.words_changed = Some(ret_words_changed);
4235        }
4236        if !ret_images_changed.is_empty() {
4237            ret.images_changed = Some(ret_images_changed);
4238        }
4239        if !ret_image_masks_changed.is_empty() {
4240            ret.image_masks_changed = Some(ret_image_masks_changed);
4241        }
4242        if !ret_css_properties_changed.is_empty() {
4243            ret.css_properties_changed = Some(ret_css_properties_changed);
4244        }
4245        if !ret_nodes_scrolled_in_callbacks.is_empty() {
4246            ret.nodes_scrolled_in_callbacks = Some(ret_nodes_scrolled_in_callbacks);
4247        }
4248
4249        if let Some(ft) = new_focus_target {
4250            if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
4251                &ft,
4252                &self.layout_results,
4253                self.focus_manager.get_focused_node().copied(),
4254            ) {
4255                ret.update_focused_node = match new_focus_node {
4256                    Some(node) => FocusUpdateRequest::FocusNode(node),
4257                    None => FocusUpdateRequest::ClearFocus,
4258                };
4259            }
4260        }
4261
4262        return ret;
4263    }
4264    
4265    /// Set the system style for resolving system color keywords in CSS.
4266    ///
4267    /// This should be called during window initialization and whenever the system
4268    /// theme changes (dark/light mode switch, accent color change).
4269    ///
4270    /// The system style is used to resolve CSS system colors like `selection-background`,
4271    /// `selection-text`, `accent`, etc. If not set, hard-coded fallback values are used.
4272    pub fn set_system_style(&mut self, system_style: std::sync::Arc<azul_css::system::SystemStyle>) {
4273        #[cfg(feature = "icu")]
4274        {
4275            self.icu_localizer = crate::icu::IcuLocalizerHandle::from_system_language(&system_style.language);
4276        }
4277        self.system_style = Some(system_style);
4278    }
4279}
4280
4281// --- ICU4X Internationalization API ---
4282
4283#[cfg(feature = "icu")]
4284impl LayoutWindow {
4285    /// Initialize the ICU localizer with the system's detected language.
4286    ///
4287    /// This should be called during window initialization, passing the language
4288    /// from `SystemStyle::language`.
4289    ///
4290    /// # Arguments
4291    /// * `locale` - The BCP 47 language tag (e.g., "en-US", "de-DE")
4292    pub fn set_icu_locale(&mut self, locale: &str) {
4293        self.icu_localizer.set_locale(locale);
4294    }
4295
4296    /// Initialize the ICU localizer from a SystemStyle.
4297    ///
4298    /// This is a convenience method that extracts the language from the system style.
4299    pub fn init_icu_from_system_style(&mut self, system_style: &azul_css::system::SystemStyle) {
4300        self.icu_localizer = IcuLocalizerHandle::from_system_language(&system_style.language);
4301    }
4302
4303    /// Get a clone of the ICU localizer handle.
4304    ///
4305    /// This can be used to perform locale-aware formatting outside of callbacks.
4306    pub fn get_icu_localizer(&self) -> IcuLocalizerHandle {
4307        self.icu_localizer.clone()
4308    }
4309
4310    /// Load additional ICU locale data from a binary blob.
4311    ///
4312    /// The blob should be generated using `icu4x-datagen` with the `--format blob` flag.
4313    /// This allows supporting locales that aren't compiled into the binary.
4314    pub fn load_icu_data_blob(&mut self, data: Vec<u8>) -> bool {
4315        self.icu_localizer.load_data_blob(&data)
4316    }
4317}
4318
4319#[cfg(test)]
4320mod tests {
4321    use super::*;
4322    use crate::{thread::Thread, timer::Timer};
4323
4324    #[test]
4325    fn test_timer_add_remove() {
4326        let fc_cache = FcFontCache::default();
4327        let mut window = LayoutWindow::new(fc_cache).unwrap();
4328
4329        let timer_id = TimerId { id: 1 };
4330        let timer = Timer::default();
4331
4332        // Add timer
4333        window.add_timer(timer_id, timer);
4334        assert!(window.get_timer(&timer_id).is_some());
4335        assert_eq!(window.get_timer_ids().len(), 1);
4336
4337        // Remove timer
4338        let removed = window.remove_timer(&timer_id);
4339        assert!(removed.is_some());
4340        assert!(window.get_timer(&timer_id).is_none());
4341        assert_eq!(window.get_timer_ids().len(), 0);
4342    }
4343
4344    #[test]
4345    fn test_timer_get_mut() {
4346        let fc_cache = FcFontCache::default();
4347        let mut window = LayoutWindow::new(fc_cache).unwrap();
4348
4349        let timer_id = TimerId { id: 1 };
4350        let timer = Timer::default();
4351
4352        window.add_timer(timer_id, timer);
4353
4354        // Get mutable reference
4355        let timer_mut = window.get_timer_mut(&timer_id);
4356        assert!(timer_mut.is_some());
4357    }
4358
4359    #[test]
4360    fn test_multiple_timers() {
4361        let fc_cache = FcFontCache::default();
4362        let mut window = LayoutWindow::new(fc_cache).unwrap();
4363
4364        let timer1 = TimerId { id: 1 };
4365        let timer2 = TimerId { id: 2 };
4366        let timer3 = TimerId { id: 3 };
4367
4368        window.add_timer(timer1, Timer::default());
4369        window.add_timer(timer2, Timer::default());
4370        window.add_timer(timer3, Timer::default());
4371
4372        assert_eq!(window.get_timer_ids().len(), 3);
4373
4374        window.remove_timer(&timer2);
4375        assert_eq!(window.get_timer_ids().len(), 2);
4376        assert!(window.get_timer(&timer1).is_some());
4377        assert!(window.get_timer(&timer2).is_none());
4378        assert!(window.get_timer(&timer3).is_some());
4379    }
4380
4381    // Thread management tests removed - Thread::default() not available
4382    // and threads require complex setup. Thread management is tested
4383    // through integration tests instead.
4384
4385    #[test]
4386    fn test_gpu_cache_management() {
4387        let fc_cache = FcFontCache::default();
4388        let mut window = LayoutWindow::new(fc_cache).unwrap();
4389
4390        let dom_id = DomId { inner: 0 };
4391
4392        // Initially empty
4393        assert!(window.get_gpu_cache(&dom_id).is_none());
4394
4395        // Get or create
4396        let cache = window.get_or_create_gpu_cache(dom_id);
4397        assert!(cache.transform_keys.is_empty());
4398
4399        // Now exists
4400        assert!(window.get_gpu_cache(&dom_id).is_some());
4401
4402        // Can get mutable reference
4403        let cache_mut = window.get_gpu_cache_mut(&dom_id);
4404        assert!(cache_mut.is_some());
4405    }
4406
4407    #[test]
4408    fn test_gpu_cache_multiple_doms() {
4409        let fc_cache = FcFontCache::default();
4410        let mut window = LayoutWindow::new(fc_cache).unwrap();
4411
4412        let dom1 = DomId { inner: 0 };
4413        let dom2 = DomId { inner: 1 };
4414
4415        window.get_or_create_gpu_cache(dom1);
4416        window.get_or_create_gpu_cache(dom2);
4417
4418        assert!(window.get_gpu_cache(&dom1).is_some());
4419        assert!(window.get_gpu_cache(&dom2).is_some());
4420    }
4421
4422    #[test]
4423    fn test_compute_cursor_type_empty_hit_test() {
4424        use crate::hit_test::FullHitTest;
4425
4426        let fc_cache = FcFontCache::default();
4427        let window = LayoutWindow::new(fc_cache).unwrap();
4428
4429        let empty_hit = FullHitTest::empty(None);
4430        let cursor_test = window.compute_cursor_type_hit_test(&empty_hit);
4431
4432        // Empty hit test should result in default cursor
4433        assert_eq!(
4434            cursor_test.cursor_icon,
4435            azul_core::window::MouseCursorType::Default
4436        );
4437        assert!(cursor_test.cursor_node.is_none());
4438    }
4439
4440    #[test]
4441    fn test_layout_result_access() {
4442        let fc_cache = FcFontCache::default();
4443        let window = LayoutWindow::new(fc_cache).unwrap();
4444
4445        let dom_id = DomId { inner: 0 };
4446
4447        // Initially no layout results
4448        assert!(window.get_layout_result(&dom_id).is_none());
4449        assert_eq!(window.get_dom_ids().len(), 0);
4450    }
4451
4452    // ScrollManager and IFrame Integration Tests
4453
4454    #[test]
4455    fn test_scroll_manager_initialization() {
4456        let fc_cache = FcFontCache::default();
4457        let window = LayoutWindow::new(fc_cache).unwrap();
4458
4459        let dom_id = DomId::ROOT_ID;
4460        let node_id = NodeId::new(0);
4461
4462        // Initially no scroll states
4463        let scroll_offsets = window.scroll_manager.get_scroll_states_for_dom(dom_id);
4464        assert!(scroll_offsets.is_empty());
4465
4466        // No current offset
4467        let offset = window.scroll_manager.get_current_offset(dom_id, node_id);
4468        assert_eq!(offset, None);
4469    }
4470
4471    #[test]
4472    fn test_scroll_manager_tick_updates_activity() {
4473        let fc_cache = FcFontCache::default();
4474        let mut window = LayoutWindow::new(fc_cache).unwrap();
4475
4476        let dom_id = DomId::ROOT_ID;
4477        let node_id = NodeId::new(0);
4478
4479        // Create a scroll event
4480        let scroll_event = crate::managers::scroll_state::ScrollEvent {
4481            dom_id,
4482            node_id,
4483            delta: LogicalPosition::new(10.0, 20.0),
4484            source: azul_core::events::EventSource::User,
4485            duration: None,
4486            easing: EasingFunction::Linear,
4487        };
4488
4489        #[cfg(feature = "std")]
4490        let now = Instant::System(std::time::Instant::now().into());
4491        #[cfg(not(feature = "std"))]
4492        let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
4493
4494        let did_scroll = window
4495            .scroll_manager
4496            .process_scroll_event(scroll_event, now.clone());
4497
4498        // process_scroll_event should return true for successful scroll
4499        assert!(did_scroll);
4500    }
4501
4502    #[test]
4503    fn test_scroll_manager_programmatic_scroll() {
4504        let fc_cache = FcFontCache::default();
4505        let mut window = LayoutWindow::new(fc_cache).unwrap();
4506
4507        let dom_id = DomId::ROOT_ID;
4508        let node_id = NodeId::new(0);
4509
4510        #[cfg(feature = "std")]
4511        let now = Instant::System(std::time::Instant::now().into());
4512        #[cfg(not(feature = "std"))]
4513        let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
4514
4515        // Programmatic scroll with animation
4516        window.scroll_manager.scroll_to(
4517            dom_id,
4518            node_id,
4519            LogicalPosition::new(100.0, 200.0),
4520            Duration::System(SystemTimeDiff::from_millis(300)),
4521            EasingFunction::EaseOut,
4522            now.clone(),
4523        );
4524
4525        let tick_result = window.scroll_manager.tick(now);
4526
4527        // Programmatic scroll should start animation
4528        assert!(tick_result.needs_repaint);
4529    }
4530
4531    #[test]
4532    fn test_scroll_manager_iframe_edge_detection() {
4533        // Note: This test is disabled because the new IFrame architecture
4534        // moved edge detection logic to IFrameManager. The old ScrollManager
4535        // API (update_iframe_scroll_info, iframes_to_update) no longer exists.
4536        // Edge detection is now tested through IFrameManager::check_reinvoke.
4537
4538        // TODO: Rewrite this test to use the new IFrameManager API once
4539        // we have a proper test setup for IFrames.
4540    }
4541
4542    #[test]
4543    fn test_scroll_manager_iframe_invocation_tracking() {
4544        // Note: This test is disabled because IFrame invocation tracking
4545        // moved to IFrameManager. The ScrollManager no longer tracks
4546        // which IFrames have been invoked.
4547
4548        // TODO: Rewrite this test to use IFrameManager::mark_invoked
4549        // and IFrameManager::check_reinvoke.
4550    }
4551
4552    #[test]
4553    fn test_scrollbar_opacity_fading() {
4554        // Note: This test is disabled because scrollbar opacity calculation
4555        // is now done through a helper function in LayoutWindow, not
4556        // through ScrollManager.get_scrollbar_opacity().
4557
4558        // The new architecture separates scroll state from opacity calculation.
4559        // ScrollManager tracks last_activity_time, and LayoutWindow has a
4560        // calculate_scrollbar_opacity() helper that computes fade based on time.
4561
4562        // TODO: Rewrite this test to use LayoutWindow::calculate_scrollbar_opacity
4563        // with ScrollManager::get_last_activity_time.
4564    }
4565
4566    #[test]
4567    fn test_iframe_callback_reason_initial_render() {
4568        // Note: This test is disabled because the frame lifecycle API
4569        // (begin_frame, end_frame, had_new_doms) was removed from ScrollManager.
4570
4571        // IFrame callback reasons are now determined by IFrameManager::check_reinvoke
4572        // which checks if an IFrame has been invoked before.
4573
4574        // TODO: Rewrite to test IFrameManager::check_reinvoke with InitialRender.
4575    }
4576
4577    #[test]
4578    fn test_gpu_cache_scrollbar_opacity_keys() {
4579        let fc_cache = FcFontCache::default();
4580        let mut window = LayoutWindow::new(fc_cache).unwrap();
4581
4582        let dom_id = DomId::ROOT_ID;
4583        let node_id = NodeId::new(0);
4584
4585        // Get or create GPU cache
4586        let gpu_cache = window.get_or_create_gpu_cache(dom_id);
4587
4588        // Initially no scrollbar opacity keys
4589        assert!(gpu_cache.scrollbar_v_opacity_keys.is_empty());
4590        assert!(gpu_cache.scrollbar_h_opacity_keys.is_empty());
4591
4592        // Add a vertical scrollbar opacity key
4593        let opacity_key = azul_core::resources::OpacityKey::unique();
4594        gpu_cache
4595            .scrollbar_v_opacity_keys
4596            .insert((dom_id, node_id), opacity_key);
4597        gpu_cache
4598            .scrollbar_v_opacity_values
4599            .insert((dom_id, node_id), 1.0);
4600
4601        // Verify it was added
4602        assert_eq!(gpu_cache.scrollbar_v_opacity_keys.len(), 1);
4603        assert_eq!(
4604            gpu_cache.scrollbar_v_opacity_values.get(&(dom_id, node_id)),
4605            Some(&1.0)
4606        );
4607    }
4608
4609    #[test]
4610    fn test_frame_lifecycle_begin_end() {
4611        // Note: This test is disabled because begin_frame/end_frame API
4612        // was removed from ScrollManager. Frame lifecycle is now managed
4613        // at a higher level.
4614
4615        // The new ScrollManager focuses purely on scroll state and animations.
4616        // Frame tracking (had_scroll_activity, had_programmatic_scroll) was
4617        // removed as it's no longer needed with the new architecture.
4618
4619        // TODO: If frame lifecycle tracking is needed, it should be
4620        // implemented at the LayoutWindow level, not in ScrollManager.
4621    }
4622}
4623
4624// --- Cross-Paragraph Cursor Navigation API ---
4625impl LayoutWindow {
4626    /// Finds the next text node in the DOM tree after the given node.
4627    ///
4628    /// This function performs a depth-first traversal to find the next node
4629    /// that contains text content and is selectable (user-select != none).
4630    ///
4631    /// # Arguments
4632    /// * `dom_id` - The ID of the DOM containing the current node
4633    /// * `current_node` - The current node ID to start searching from
4634    ///
4635    /// # Returns
4636    /// * `Some((DomId, NodeId))` - The next text node if found
4637    /// * `None` - If no next text node exists
4638    pub fn find_next_text_node(
4639        &self,
4640        dom_id: &DomId,
4641        current_node: NodeId,
4642    ) -> Option<(DomId, NodeId)> {
4643        let layout_result = self.get_layout_result(dom_id)?;
4644        let styled_dom = &layout_result.styled_dom;
4645
4646        // Start from the next node in document order
4647        let start_idx = current_node.index() + 1;
4648        let node_hierarchy = &styled_dom.node_hierarchy;
4649
4650        for i in start_idx..node_hierarchy.len() {
4651            let node_id = NodeId::new(i);
4652
4653            // Check if node has text content
4654            if self.node_has_text_content(styled_dom, node_id) {
4655                // Check if text is selectable
4656                if self.is_text_selectable(styled_dom, node_id) {
4657                    return Some((*dom_id, node_id));
4658                }
4659            }
4660        }
4661
4662        None
4663    }
4664
4665    /// Finds the previous text node in the DOM tree before the given node.
4666    ///
4667    /// This function performs a reverse depth-first traversal to find the previous node
4668    /// that contains text content and is selectable.
4669    ///
4670    /// # Arguments
4671    /// * `dom_id` - The ID of the DOM containing the current node
4672    /// * `current_node` - The current node ID to start searching from
4673    ///
4674    /// # Returns
4675    /// * `Some((DomId, NodeId))` - The previous text node if found
4676    /// * `None` - If no previous text node exists
4677    pub fn find_prev_text_node(
4678        &self,
4679        dom_id: &DomId,
4680        current_node: NodeId,
4681    ) -> Option<(DomId, NodeId)> {
4682        let layout_result = self.get_layout_result(dom_id)?;
4683        let styled_dom = &layout_result.styled_dom;
4684
4685        // Start from the previous node in reverse document order
4686        let current_idx = current_node.index();
4687
4688        for i in (0..current_idx).rev() {
4689            let node_id = NodeId::new(i);
4690
4691            // Check if node has text content
4692            if self.node_has_text_content(styled_dom, node_id) {
4693                // Check if text is selectable
4694                if self.is_text_selectable(styled_dom, node_id) {
4695                    return Some((*dom_id, node_id));
4696                }
4697            }
4698        }
4699
4700        None
4701    }
4702
4703    /// Find the last text child node of a given node.
4704    /// 
4705    /// For contenteditable elements, the text is usually in a child Text node,
4706    /// not the contenteditable div itself. This function finds the last Text node
4707    /// so the cursor defaults to the end position.
4708    fn find_last_text_child(&self, dom_id: DomId, parent_node_id: NodeId) -> Option<NodeId> {
4709        let layout_result = self.layout_results.get(&dom_id)?;
4710        let styled_dom = &layout_result.styled_dom;
4711        let node_data_container = styled_dom.node_data.as_container();
4712        let hierarchy_container = styled_dom.node_hierarchy.as_container();
4713        
4714        // Check if parent itself is a text node
4715        let parent_type = node_data_container[parent_node_id].get_node_type();
4716        if matches!(parent_type, NodeType::Text(_)) {
4717            return Some(parent_node_id);
4718        }
4719        
4720        // Find the last text child by iterating through all children
4721        let parent_item = &hierarchy_container[parent_node_id];
4722        let mut last_text_child: Option<NodeId> = None;
4723        let mut current_child = parent_item.first_child_id(parent_node_id);
4724        while let Some(child_id) = current_child {
4725            let child_type = node_data_container[child_id].get_node_type();
4726            if matches!(child_type, NodeType::Text(_)) {
4727                last_text_child = Some(child_id);
4728            }
4729            current_child = hierarchy_container[child_id].next_sibling_id();
4730        }
4731        
4732        last_text_child
4733    }
4734
4735    /// Checks if a node has text content.
4736    fn node_has_text_content(&self, styled_dom: &StyledDom, node_id: NodeId) -> bool {
4737        // Check if node itself is a text node
4738        let node_data_container = styled_dom.node_data.as_container();
4739        let node_type = node_data_container[node_id].get_node_type();
4740        if matches!(node_type, NodeType::Text(_)) {
4741            return true;
4742        }
4743
4744        // Check if node has text children
4745        let hierarchy_container = styled_dom.node_hierarchy.as_container();
4746        let node_item = &hierarchy_container[node_id];
4747
4748        // Iterate through children
4749        let mut current_child = node_item.first_child_id(node_id);
4750        while let Some(child_id) = current_child {
4751            let child_type = node_data_container[child_id].get_node_type();
4752            if matches!(child_type, NodeType::Text(_)) {
4753                return true;
4754            }
4755
4756            // Move to next sibling
4757            current_child = hierarchy_container[child_id].next_sibling_id();
4758        }
4759
4760        false
4761    }
4762
4763    /// Checks if text in a node is selectable based on CSS user-select property.
4764    fn is_text_selectable(&self, styled_dom: &StyledDom, node_id: NodeId) -> bool {
4765        let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
4766        crate::solver3::getters::is_text_selectable(styled_dom, node_id, node_state)
4767    }
4768
4769    /// Process an accessibility action from an assistive technology.
4770    ///
4771    /// This method dispatches actions to the appropriate managers (scroll, focus, etc.)
4772    /// and returns information about which nodes were affected and how.
4773    ///
4774    /// # Arguments
4775    /// * `dom_id` - The DOM containing the target node
4776    /// * `node_id` - The target node for the action
4777    /// * `action` - The accessibility action to perform
4778    /// * `now` - Current timestamp for animations
4779    ///
4780    /// # Returns
4781    /// A BTreeMap of affected nodes with:
4782    /// - Key: DomNodeId that was affected
4783    /// - Value: (Vec<EventFilter> synthetic events to dispatch, bool indicating if node needs
4784    ///   re-layout)
4785    ///
4786    /// Empty map = action was not applicable or nothing changed
4787    #[cfg(feature = "a11y")]
4788    pub fn process_accessibility_action(
4789        &mut self,
4790        dom_id: DomId,
4791        node_id: NodeId,
4792        action: azul_core::dom::AccessibilityAction,
4793        now: std::time::Instant,
4794    ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
4795        use crate::managers::text_input::TextInputSource;
4796
4797        let mut affected_nodes = BTreeMap::new();
4798
4799        match action {
4800            // Focus actions
4801            AccessibilityAction::Focus => {
4802                let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4803                let dom_node_id = DomNodeId {
4804                    dom: dom_id,
4805                    node: hierarchy_id,
4806                };
4807                self.focus_manager.set_focused_node(Some(dom_node_id));
4808
4809                // Check if node is contenteditable - if so, initialize cursor at end of text
4810                if let Some(layout_result) = self.layout_results.get(&dom_id) {
4811                    if let Some(styled_node) = layout_result
4812                        .styled_dom
4813                        .node_data
4814                        .as_ref()
4815                        .get(node_id.index())
4816                    {
4817                        // Check BOTH: the contenteditable boolean field AND the attribute
4818                        // NodeData has a direct `contenteditable: bool` field that should be
4819                        // checked in addition to the attribute for robustness
4820                        let is_contenteditable = styled_node.contenteditable
4821                            || styled_node.attributes.as_ref().iter().any(|attr| {
4822                                matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
4823                            });
4824                        
4825                        if is_contenteditable {
4826                            // Get inline layout for cursor positioning
4827                            // Clone the Arc to avoid borrow conflict
4828                            let inline_layout = self.get_inline_layout_for_node(dom_id, node_id).cloned();
4829                            if inline_layout.is_some() {
4830                                self.cursor_manager.initialize_cursor_at_end(
4831                                    dom_id,
4832                                    node_id,
4833                                    inline_layout.as_ref(),
4834                                );
4835
4836                                // Scroll cursor into view if necessary
4837                                self.scroll_cursor_into_view_if_needed(dom_id, node_id, now);
4838                            }
4839                        } else {
4840                            // Not editable - clear cursor
4841                            self.cursor_manager.clear();
4842                        }
4843                    }
4844                }
4845
4846                // Optionally scroll into view
4847                self.scroll_to_node_if_needed(dom_id, node_id, now);
4848            }
4849            AccessibilityAction::Blur => {
4850                self.focus_manager.clear_focus();
4851                self.cursor_manager.clear();
4852            }
4853            AccessibilityAction::SetSequentialFocusNavigationStartingPoint => {
4854                let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4855                let dom_node_id = DomNodeId {
4856                    dom: dom_id,
4857                    node: hierarchy_id,
4858                };
4859                self.focus_manager.set_focused_node(Some(dom_node_id));
4860                // Clear cursor for focus navigation
4861                self.cursor_manager.clear();
4862            }
4863
4864            // Scroll actions
4865            AccessibilityAction::ScrollIntoView => {
4866                self.scroll_to_node_if_needed(dom_id, node_id, now);
4867            }
4868            AccessibilityAction::ScrollUp => {
4869                self.scroll_manager.scroll_by(
4870                    dom_id,
4871                    node_id,
4872                    LogicalPosition { x: 0.0, y: -100.0 },
4873                    std::time::Duration::from_millis(200).into(),
4874                    azul_core::events::EasingFunction::EaseOut,
4875                    now.into(),
4876                );
4877            }
4878            AccessibilityAction::ScrollDown => {
4879                self.scroll_manager.scroll_by(
4880                    dom_id,
4881                    node_id,
4882                    LogicalPosition { x: 0.0, y: 100.0 },
4883                    std::time::Duration::from_millis(200).into(),
4884                    azul_core::events::EasingFunction::EaseOut,
4885                    now.into(),
4886                );
4887            }
4888            AccessibilityAction::ScrollLeft => {
4889                self.scroll_manager.scroll_by(
4890                    dom_id,
4891                    node_id,
4892                    LogicalPosition { x: -100.0, y: 0.0 },
4893                    std::time::Duration::from_millis(200).into(),
4894                    azul_core::events::EasingFunction::EaseOut,
4895                    now.into(),
4896                );
4897            }
4898            AccessibilityAction::ScrollRight => {
4899                self.scroll_manager.scroll_by(
4900                    dom_id,
4901                    node_id,
4902                    LogicalPosition { x: 100.0, y: 0.0 },
4903                    std::time::Duration::from_millis(200).into(),
4904                    azul_core::events::EasingFunction::EaseOut,
4905                    now.into(),
4906                );
4907            }
4908            AccessibilityAction::ScrollUp => {
4909                // Scroll up by default amount (could use page size for page up)
4910                if let Some(size) = self.get_node_used_size_a11y(dom_id, node_id) {
4911                    let scroll_amount = size.height.min(100.0); // Scroll by 100px or page height
4912                    self.scroll_manager.scroll_by(
4913                        dom_id,
4914                        node_id,
4915                        LogicalPosition {
4916                            x: 0.0,
4917                            y: -scroll_amount,
4918                        },
4919                        std::time::Duration::from_millis(300).into(),
4920                        azul_core::events::EasingFunction::EaseInOut,
4921                        now.into(),
4922                    );
4923                }
4924            }
4925            AccessibilityAction::ScrollDown => {
4926                // Scroll down by default amount (could use page size for page down)
4927                if let Some(size) = self.get_node_used_size_a11y(dom_id, node_id) {
4928                    let scroll_amount = size.height.min(100.0); // Scroll by 100px or page height
4929                    self.scroll_manager.scroll_by(
4930                        dom_id,
4931                        node_id,
4932                        LogicalPosition {
4933                            x: 0.0,
4934                            y: scroll_amount,
4935                        },
4936                        std::time::Duration::from_millis(300).into(),
4937                        azul_core::events::EasingFunction::EaseInOut,
4938                        now.into(),
4939                    );
4940                }
4941            }
4942            AccessibilityAction::SetScrollOffset(pos) => {
4943                self.scroll_manager.scroll_to(
4944                    dom_id,
4945                    node_id,
4946                    pos,
4947                    std::time::Duration::from_millis(0).into(),
4948                    azul_core::events::EasingFunction::Linear,
4949                    now.into(),
4950                );
4951            }
4952            AccessibilityAction::ScrollToPoint(pos) => {
4953                self.scroll_manager.scroll_to(
4954                    dom_id,
4955                    node_id,
4956                    pos,
4957                    std::time::Duration::from_millis(300).into(),
4958                    azul_core::events::EasingFunction::EaseInOut,
4959                    now.into(),
4960                );
4961            }
4962
4963            // Actions that should trigger element callbacks if they exist
4964            // These generate synthetic EventFilters that go through the normal
4965            // callback system
4966            AccessibilityAction::Default => {
4967                // Default action → synthetic Click event
4968                let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4969                let dom_node_id = DomNodeId {
4970                    dom: dom_id,
4971                    node: hierarchy_id,
4972                };
4973
4974                // Check if node has a Default callback, otherwise fallback to Click
4975                let event_filter = if let Some(layout_result) = self.layout_results.get(&dom_id) {
4976                    if let Some(styled_node) = layout_result
4977                        .styled_dom
4978                        .node_data
4979                        .as_ref()
4980                        .get(node_id.index())
4981                    {
4982                        let has_default_callback =
4983                            styled_node.callbacks.as_ref().iter().any(|cb| {
4984                                // On::Default converts to HoverEventFilter::MouseUp
4985                                matches!(cb.event, EventFilter::Hover(HoverEventFilter::MouseUp))
4986                            });
4987
4988                        if has_default_callback {
4989                            EventFilter::Hover(HoverEventFilter::MouseUp)
4990                        } else {
4991                            EventFilter::Hover(HoverEventFilter::MouseUp)
4992                        }
4993                    } else {
4994                        EventFilter::Hover(HoverEventFilter::MouseUp)
4995                    }
4996                } else {
4997                    EventFilter::Hover(HoverEventFilter::MouseUp)
4998                };
4999
5000                affected_nodes.insert(dom_node_id, (vec![event_filter], false));
5001            }
5002
5003            AccessibilityAction::Increment | AccessibilityAction::Decrement => {
5004                // Increment/Decrement work by:
5005                // 1. Reading the current value (from "value" attribute or text content)
5006                // 2. Parsing it as a number
5007                // 3. Incrementing/decrementing by 1
5008                // 4. Converting back to string
5009                // 5. Recording as text input (fires TextInput event)
5010                //
5011                // This allows user callbacks to intercept via On::TextInput
5012
5013                let is_increment = matches!(action, AccessibilityAction::Increment);
5014
5015                // Get the current value
5016                let current_value = if let Some(layout_result) = self.layout_results.get(&dom_id) {
5017                    if let Some(styled_node) = layout_result
5018                        .styled_dom
5019                        .node_data
5020                        .as_ref()
5021                        .get(node_id.index())
5022                    {
5023                        // Try "value" attribute first
5024                        styled_node
5025                            .attributes
5026                            .as_ref()
5027                            .iter()
5028                            .find_map(|attr| {
5029                                if let AttributeType::Value(v) = attr {
5030                                    Some(v.as_str().to_string())
5031                                } else {
5032                                    None
5033                                }
5034                            })
5035                            .or_else(|| {
5036                                // Fallback to text content
5037                                if let NodeType::Text(text) = styled_node.get_node_type() {
5038                                    Some(text.as_str().to_string())
5039                                } else {
5040                                    None
5041                                }
5042                            })
5043                    } else {
5044                        None
5045                    }
5046                } else {
5047                    None
5048                };
5049
5050                // Parse as number, increment/decrement, convert back to string
5051                if let Some(value_str) = current_value {
5052                    let parsed: Result<f64, _> = value_str.trim().parse();
5053
5054                    let new_value_str = if let Ok(num) = parsed {
5055                        // Successfully parsed as number
5056                        let new_num = if is_increment { num + 1.0 } else { num - 1.0 };
5057                        // Format with same precision as input if possible
5058                        if num.fract() == 0.0 {
5059                            format!("{}", new_num as i64)
5060                        } else {
5061                            format!("{}", new_num)
5062                        }
5063                    } else {
5064                        // Not a number - treat as 0 and increment/decrement
5065                        if is_increment {
5066                            "1".to_string()
5067                        } else {
5068                            "-1".to_string()
5069                        }
5070                    };
5071
5072                    // Record as text input (will fire On::TextInput callbacks)
5073                    let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5074                    let dom_node_id = DomNodeId {
5075                        dom: dom_id,
5076                        node: hierarchy_id,
5077                    };
5078
5079                    // Get old text for changeset
5080                    let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
5081                    let old_text = self.extract_text_from_inline_content(&old_inline_content);
5082
5083                    // Record the text input
5084                    self.text_input_manager.record_input(
5085                        dom_node_id,
5086                        new_value_str,
5087                        old_text,
5088                        TextInputSource::Accessibility,
5089                    );
5090
5091                    // Add TextInput event to affected nodes
5092                    affected_nodes.insert(
5093                        dom_node_id,
5094                        (vec![EventFilter::Focus(FocusEventFilter::TextInput)], false),
5095                    );
5096                }
5097            }
5098
5099            AccessibilityAction::Collapse | AccessibilityAction::Expand => {
5100                // Map to corresponding On:: events
5101                let event_type = match action {
5102                    AccessibilityAction::Collapse => On::Collapse,
5103                    AccessibilityAction::Expand => On::Expand,
5104                    _ => unreachable!(),
5105                };
5106
5107                // Check if node has a callback for this event type
5108                if let Some(layout_result) = self.layout_results.get(&dom_id) {
5109                    if let Some(styled_node) = layout_result
5110                        .styled_dom
5111                        .node_data
5112                        .as_ref()
5113                        .get(node_id.index())
5114                    {
5115                        // Check if any callback matches this event type
5116                        let has_callback = styled_node
5117                            .callbacks
5118                            .as_ref()
5119                            .iter()
5120                            .any(|cb| cb.event == event_type.into());
5121
5122                        let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5123                        let dom_node_id = DomNodeId {
5124                            dom: dom_id,
5125                            node: hierarchy_id,
5126                        };
5127
5128                        if has_callback {
5129                            // Generate EventFilter for this specific callback
5130                            affected_nodes.insert(dom_node_id, (vec![event_type.into()], false));
5131                        } else {
5132                            // No specific callback - fallback to regular Click
5133                            affected_nodes.insert(
5134                                dom_node_id,
5135                                (vec![EventFilter::Hover(HoverEventFilter::MouseUp)], false),
5136                            );
5137                        }
5138                    }
5139                }
5140            }
5141
5142            // Context menu - check if node has a menu and trigger right-click event
5143            AccessibilityAction::ShowContextMenu => {
5144                // Check if the node has a context menu attached
5145                let layout_result = match self.layout_results.get(&dom_id) {
5146                    Some(lr) => lr,
5147                    None => {
5148                        return affected_nodes;
5149                    }
5150                };
5151
5152                // Get the node from the styled DOM
5153                let styled_node = match layout_result
5154                    .styled_dom
5155                    .node_data
5156                    .as_ref()
5157                    .get(node_id.index())
5158                {
5159                    Some(node) => node,
5160                    None => {
5161                        return affected_nodes;
5162                    }
5163                };
5164
5165                // Check if node has context menu
5166                let has_context_menu = styled_node.get_context_menu().is_some();
5167
5168                if has_context_menu {
5169                    // TODO: Generate synthetic right-click event to trigger context menu
5170                    // This requires access to the event system which is not available here
5171                } else {
5172                    // No context menu attached to node - silently ignore
5173                }
5174            }
5175
5176            // Text editing actions - use text3/edit.rs
5177            AccessibilityAction::ReplaceSelectedText(ref text) => {
5178                let nodes = self.edit_text_node(
5179                    dom_id,
5180                    node_id,
5181                    TextEditType::ReplaceSelection(text.as_str().to_string()),
5182                );
5183                for node in nodes {
5184                    affected_nodes.insert(node, (Vec::new(), true)); // true = needs re-layout
5185                }
5186            }
5187            AccessibilityAction::SetValue(ref text) => {
5188                let nodes = self.edit_text_node(
5189                    dom_id,
5190                    node_id,
5191                    TextEditType::SetValue(text.as_str().to_string()),
5192                );
5193                for node in nodes {
5194                    affected_nodes.insert(node, (Vec::new(), true));
5195                }
5196            }
5197            AccessibilityAction::SetNumericValue(value) => {
5198                let nodes = self.edit_text_node(
5199                    dom_id,
5200                    node_id,
5201                    TextEditType::SetNumericValue(value.get() as f64),
5202                );
5203                for node in nodes {
5204                    affected_nodes.insert(node, (Vec::new(), true));
5205                }
5206            }
5207            AccessibilityAction::SetTextSelection(selection) => {
5208                // Get the text layout for this node from the layout tree
5209                let text_layout = self.get_node_inline_layout(dom_id, node_id);
5210
5211                if let Some(inline_layout) = text_layout {
5212                    // Convert byte offsets to TextCursor positions
5213                    let start_cursor = self.byte_offset_to_cursor(
5214                        inline_layout.as_ref(),
5215                        selection.selection_start as u32,
5216                    );
5217                    let end_cursor = self.byte_offset_to_cursor(
5218                        inline_layout.as_ref(),
5219                        selection.selection_end as u32,
5220                    );
5221
5222                    if let (Some(start), Some(end)) = (start_cursor, end_cursor) {
5223                        let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5224                        let dom_node_id = DomNodeId {
5225                            dom: dom_id,
5226                            node: hierarchy_id,
5227                        };
5228
5229                        if start == end {
5230                            // Same position - just set cursor
5231                            self.cursor_manager.move_cursor_to(start, dom_id, node_id);
5232
5233                            // Clear any existing selections
5234                            self.selection_manager.clear_selection(&dom_id);
5235                        } else {
5236                            // Different positions - create selection range
5237                            let selection = Selection::Range(SelectionRange { start, end });
5238
5239                            let selection_state = SelectionState {
5240                                selections: vec![selection].into(),
5241                                node_id: dom_node_id,
5242                            };
5243
5244                            // Set selection in SelectionManager
5245                            self.selection_manager
5246                                .set_selection(dom_id, selection_state);
5247
5248                            // Also set cursor to start of selection
5249                            self.cursor_manager.move_cursor_to(start, dom_id, node_id);
5250                        }
5251                    } else {
5252                        // Could not convert byte offsets to cursors - silently ignore
5253                    }
5254                } else {
5255                    // No text layout available for node - silently ignore
5256                }
5257            }
5258
5259            // Tooltip actions
5260            AccessibilityAction::ShowTooltip | AccessibilityAction::HideTooltip => {
5261                // TODO: Integrate with tooltip manager when implemented
5262            }
5263
5264            AccessibilityAction::CustomAction(_id) => {
5265                // TODO: Allow custom action handlers
5266            }
5267        }
5268
5269        // Sync cursor to selection manager for rendering
5270        self.sync_cursor_to_selection_manager();
5271
5272        affected_nodes
5273    }
5274
5275    /// Process text input from keyboard using cursor/selection/focus managers.
5276    ///
5277    /// This is the new unified text input handling. The framework manages text editing
5278    /// internally using managers, then fires callbacks (On::TextInput, On::Changed)
5279    /// after the internal state is already updated.
5280    ///
5281    /// ## Workflow
5282    /// 1. Check if focus manager has a focused contenteditable node
5283    /// 2. Get cursor/selection from managers
5284    /// 3. Call edit_text_node to apply the edit and update cache
5285    /// 4. Collect affected nodes that need dirty marking
5286    /// 5. Return map for re-layout triggering
5287    ///
5288    /// ## Parameters
5289    /// * `text_input` - The text that was typed (can be multiple chars for IME)
5290    ///
5291    /// ## Returns
5292    /// BTreeMap of affected nodes with:
5293    /// - Key: DomNodeId that was affected
5294    /// - Value: (Vec<EventFilter> synthetic events, bool needs_relayout)
5295    /// - Empty map = no focused contenteditable node
5296    pub fn record_text_input(
5297        &mut self,
5298        text_input: &str,
5299    ) -> BTreeMap<azul_core::dom::DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
5300        use std::collections::BTreeMap;
5301
5302        use crate::managers::text_input::TextInputSource;
5303
5304        let mut affected_nodes = BTreeMap::new();
5305
5306        if text_input.is_empty() {
5307            return affected_nodes;
5308        }
5309
5310        // Get focused node
5311        let focused_node = match self.focus_manager.get_focused_node().copied() {
5312            Some(node) => node,
5313            None => {
5314                return affected_nodes;
5315            }
5316        };
5317
5318        let node_id = match focused_node.node.into_crate_internal() {
5319            Some(id) => id,
5320            None => {
5321                return affected_nodes;
5322            }
5323        };
5324
5325        // Get the OLD text before any changes
5326        let old_inline_content = self.get_text_before_textinput(focused_node.dom, node_id);
5327        let old_text = self.extract_text_from_inline_content(&old_inline_content);
5328
5329        // Record the changeset in TextInputManager (but DON'T apply changes yet)
5330        self.text_input_manager.record_input(
5331            focused_node,
5332            text_input.to_string(),
5333            old_text,
5334            TextInputSource::Keyboard, // Assuming keyboard for now
5335        );
5336
5337        // Return affected nodes with TextInput event so callbacks can be invoked
5338        let text_input_event = vec![EventFilter::Focus(FocusEventFilter::TextInput)];
5339
5340        affected_nodes.insert(focused_node, (text_input_event, false)); // false = no re-layout yet
5341
5342        affected_nodes
5343    }
5344
5345    /// Apply the recorded text changeset to the text cache
5346    ///
5347    /// This is called AFTER user callbacks, if preventDefault was not set.
5348    /// This is where we actually compute the new text and update the cache.
5349    ///
5350    /// Also updates the cursor position to reflect the edit.
5351    ///
5352    /// Returns the nodes that need to be marked dirty for re-layout.
5353    pub fn apply_text_changeset(&mut self) -> Vec<azul_core::dom::DomNodeId> {
5354        // Get the changeset from TextInputManager
5355        let changeset = match self.text_input_manager.get_pending_changeset() {
5356            Some(cs) => cs.clone(),
5357            None => {
5358                return Vec::new();
5359            }
5360        };
5361
5362        let node_id = match changeset.node.node.into_crate_internal() {
5363            Some(id) => id,
5364            None => {
5365                self.text_input_manager.clear_changeset();
5366                return Vec::new();
5367            }
5368        };
5369
5370        let dom_id = changeset.node.dom;
5371
5372        // Check if node is contenteditable
5373        let layout_result = match self.layout_results.get(&dom_id) {
5374            Some(lr) => lr,
5375            None => {
5376                self.text_input_manager.clear_changeset();
5377                return Vec::new();
5378            }
5379        };
5380
5381        let styled_node = match layout_result
5382            .styled_dom
5383            .node_data
5384            .as_ref()
5385            .get(node_id.index())
5386        {
5387            Some(node) => node,
5388            None => {
5389                self.text_input_manager.clear_changeset();
5390                return Vec::new();
5391            }
5392        };
5393
5394        // Check BOTH: the contenteditable boolean field AND the attribute
5395        // NodeData has a direct `contenteditable: bool` field that should be
5396        // checked in addition to the attribute for robustness
5397        let is_contenteditable = styled_node.contenteditable
5398            || styled_node.attributes.as_ref().iter().any(|attr| {
5399                matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
5400            });
5401
5402        if !is_contenteditable {
5403            self.text_input_manager.clear_changeset();
5404            return Vec::new();
5405        }
5406
5407        // Get the current inline content from cache
5408        let content = self.get_text_before_textinput(dom_id, node_id);
5409
5410        // Get current cursor/selection from cursor manager
5411        let current_selection = if let Some(cursor) = self.cursor_manager.get_cursor() {
5412            vec![Selection::Cursor(cursor.clone())]
5413        } else {
5414            // No cursor - create one at start of text
5415            vec![Selection::Cursor(TextCursor {
5416                cluster_id: GraphemeClusterId {
5417                    source_run: 0,
5418                    start_byte_in_run: 0,
5419                },
5420                affinity: CursorAffinity::Leading,
5421            })]
5422        };
5423
5424        // Capture pre-state for undo/redo BEFORE mutation
5425        let old_text = self.extract_text_from_inline_content(&content);
5426        let old_cursor = current_selection.first().and_then(|sel| {
5427            if let Selection::Cursor(c) = sel {
5428                Some(c.clone())
5429            } else {
5430                None
5431            }
5432        });
5433        let old_selection_range = current_selection.first().and_then(|sel| {
5434            if let Selection::Range(r) = sel {
5435                Some(*r)
5436            } else {
5437                None
5438            }
5439        });
5440
5441        let pre_state = crate::managers::undo_redo::NodeStateSnapshot {
5442            node_id: azul_core::id::NodeId::new(node_id.index()),
5443            text_content: old_text.into(),
5444            cursor_position: old_cursor.into(),
5445            selection_range: old_selection_range.into(),
5446            #[cfg(feature = "std")]
5447            timestamp: azul_core::task::Instant::System(std::time::Instant::now().into()),
5448            #[cfg(not(feature = "std"))]
5449            timestamp: azul_core::task::Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 }),
5450        };
5451
5452        // Apply the edit using text3::edit - this is a pure function
5453        use crate::text3::edit::{edit_text, TextEdit};
5454        let text_edit = TextEdit::Insert(changeset.inserted_text.as_str().to_string());
5455        let (new_content, new_selections) = edit_text(&content, &current_selection, &text_edit);
5456
5457        // Update the cursor/selection in cursor manager
5458        // This happens lazily, only when we actually apply the changes
5459        if let Some(Selection::Cursor(new_cursor)) = new_selections.first() {
5460            self.cursor_manager
5461                .move_cursor_to(new_cursor.clone(), dom_id, node_id);
5462        }
5463
5464        // Update the text cache with the new inline content
5465        self.update_text_cache_after_edit(dom_id, node_id, new_content);
5466
5467        // Record this operation to the undo/redo manager AFTER successful mutation
5468
5469        use crate::managers::changeset::{TextChangeset, TextOpInsertText, TextOperation};
5470
5471        // Get the new cursor position after edit
5472        let new_cursor = new_selections
5473            .first()
5474            .and_then(|sel| {
5475                if let Selection::Cursor(c) = sel {
5476                    // Convert TextCursor to CursorPosition
5477                    // For now, we use InWindow with approximate coordinates
5478                    // TODO: Calculate proper screen coordinates from TextCursor
5479                    Some(CursorPosition::InWindow(
5480                        azul_core::geom::LogicalPosition::new(0.0, 0.0),
5481                    ))
5482                } else {
5483                    None
5484                }
5485            })
5486            .unwrap_or(CursorPosition::Uninitialized);
5487
5488        let old_cursor_pos = old_cursor
5489            .as_ref()
5490            .map(|_| {
5491                // Convert TextCursor to CursorPosition
5492                CursorPosition::InWindow(azul_core::geom::LogicalPosition::new(0.0, 0.0))
5493            })
5494            .unwrap_or(CursorPosition::Uninitialized);
5495
5496        // Generate a unique changeset ID
5497        static CHANGESET_COUNTER: std::sync::atomic::AtomicUsize =
5498            std::sync::atomic::AtomicUsize::new(0);
5499        let changeset_id = CHANGESET_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5500
5501        let undo_changeset = TextChangeset {
5502            id: changeset_id,
5503            target: changeset.node,
5504            operation: TextOperation::InsertText(TextOpInsertText {
5505                text: changeset.inserted_text.clone(),
5506                position: old_cursor_pos,
5507                new_cursor,
5508            }),
5509            #[cfg(feature = "std")]
5510            timestamp: azul_core::task::Instant::System(std::time::Instant::now().into()),
5511            #[cfg(not(feature = "std"))]
5512            timestamp: azul_core::task::Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 }),
5513        };
5514        self.undo_redo_manager
5515            .record_operation(undo_changeset, pre_state);
5516
5517        // Clear the changeset now that it's been applied
5518        self.text_input_manager.clear_changeset();
5519
5520        // Return nodes that need dirty marking
5521        let dirty_nodes = self.determine_dirty_text_nodes(dom_id, node_id);
5522        dirty_nodes
5523    }
5524
5525    /// Determine which nodes need to be marked dirty after a text edit
5526    ///
5527    /// Returns the edited node + its parent (if it exists)
5528    fn determine_dirty_text_nodes(
5529        &self,
5530        dom_id: DomId,
5531        node_id: NodeId,
5532    ) -> Vec<azul_core::dom::DomNodeId> {
5533        let layout_result = match self.layout_results.get(&dom_id) {
5534            Some(lr) => lr,
5535            None => return Vec::new(),
5536        };
5537
5538        let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5539        let node_dom_id = azul_core::dom::DomNodeId {
5540            dom: dom_id,
5541            node: hierarchy_id,
5542        };
5543
5544        // Get parent node ID
5545        let parent_id = layout_result
5546            .styled_dom
5547            .node_hierarchy
5548            .as_container()
5549            .get(node_id)
5550            .and_then(|item| item.parent_id())
5551            .map(|parent_node_id| {
5552                let parent_hierarchy_id =
5553                    NodeHierarchyItemId::from_crate_internal(Some(parent_node_id));
5554                azul_core::dom::DomNodeId {
5555                    dom: dom_id,
5556                    node: parent_hierarchy_id,
5557                }
5558            });
5559
5560        // Return node + parent (if exists)
5561        if let Some(parent) = parent_id {
5562            vec![node_dom_id, parent]
5563        } else {
5564            vec![node_dom_id]
5565        }
5566    }
5567
5568    /// Legacy name for backward compatibility
5569    #[inline]
5570    pub fn process_text_input(
5571        &mut self,
5572        text_input: &str,
5573    ) -> BTreeMap<azul_core::dom::DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
5574        self.record_text_input(text_input)
5575    }
5576
5577    /// Get the last text changeset (what was changed in the last text input)
5578    pub fn get_last_text_changeset(&self) -> Option<&PendingTextEdit> {
5579        self.text_input_manager.get_pending_changeset()
5580    }
5581
5582    /// Get the current inline content (text before text input is applied)
5583    ///
5584    /// This is a query function that retrieves the current text state from the node.
5585    /// Returns InlineContent vector if the node has text.
5586    ///
5587    /// # Implementation Note
5588    /// This function FIRST checks `dirty_text_nodes` for optimistic state (edits not yet
5589    /// committed to StyledDom), then falls back to the StyledDom. This is critical for
5590    /// correct text input handling - without this, each keystroke would read stale state.
5591    pub fn get_text_before_textinput(&self, dom_id: DomId, node_id: NodeId) -> Vec<InlineContent> {
5592        // CRITICAL FIX: Check dirty_text_nodes first!
5593        // If the node has been edited since last full layout, its most up-to-date
5594        // content is in dirty_text_nodes, NOT in the StyledDom.
5595        // Without this check, every keystroke reads the ORIGINAL text instead of
5596        // the accumulated edits, causing bugs like double-input and wrong node affected.
5597        if let Some(dirty_node) = self.dirty_text_nodes.get(&(dom_id, node_id)) {
5598            return dirty_node.content.clone();
5599        }
5600
5601        // Fallback to committed state from StyledDom
5602        // Get the layout result for this DOM
5603        let layout_result = match self.layout_results.get(&dom_id) {
5604            Some(lr) => lr,
5605            None => return Vec::new(),
5606        };
5607
5608        // Get the node data
5609        let node_data = match layout_result
5610            .styled_dom
5611            .node_data
5612            .as_ref()
5613            .get(node_id.index())
5614        {
5615            Some(nd) => nd,
5616            None => return Vec::new(),
5617        };
5618
5619        // Extract text content from the node
5620        match node_data.get_node_type() {
5621            NodeType::Text(text) => {
5622                // Simple text node - create a single StyledRun
5623                let style = self.get_text_style_for_node(dom_id, node_id);
5624
5625                vec![InlineContent::Text(StyledRun {
5626                    text: text.as_str().to_string(),
5627                    style,
5628                    logical_start_byte: 0,
5629                    source_node_id: Some(node_id),
5630                })]
5631            }
5632            NodeType::Div | NodeType::Body | NodeType::IFrame(_) => {
5633                // Container nodes - recursively collect text from children
5634                self.collect_text_from_children(dom_id, node_id)
5635            }
5636            _ => {
5637                // Other node types (Image, etc.) don't contribute text
5638                Vec::new()
5639            }
5640        }
5641    }
5642
5643    /// Get the font style for a text node from CSS
5644    fn get_text_style_for_node(
5645        &self,
5646        dom_id: DomId,
5647        node_id: NodeId,
5648    ) -> alloc::sync::Arc<StyleProperties> {
5649        use alloc::sync::Arc;
5650
5651        let layout_result = match self.layout_results.get(&dom_id) {
5652            Some(lr) => lr,
5653            None => return Arc::new(Default::default()),
5654        };
5655
5656        // Try to get font from styled DOM
5657        let styled_nodes = layout_result.styled_dom.styled_nodes.as_ref();
5658        let _styled_node = match styled_nodes.get(node_id.index()) {
5659            Some(sn) => sn,
5660            None => return Arc::new(Default::default()),
5661        };
5662
5663        // Extract font properties from computed style
5664        // For now, use default - full implementation would query CSS property cache
5665        // TODO: Query CSS property cache for font-family, font-size, font-weight, etc.
5666        Arc::new(Default::default())
5667    }
5668
5669    /// Recursively collect text content from child nodes
5670    fn collect_text_from_children(
5671        &self,
5672        dom_id: DomId,
5673        parent_node_id: NodeId,
5674    ) -> Vec<InlineContent> {
5675        let layout_result = match self.layout_results.get(&dom_id) {
5676            Some(lr) => lr,
5677            None => return Vec::new(),
5678        };
5679
5680        let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_ref();
5681        let parent_item = match node_hierarchy.get(parent_node_id.index()) {
5682            Some(item) => item,
5683            None => return Vec::new(),
5684        };
5685
5686        let mut result = Vec::new();
5687
5688        // Traverse all children
5689        let mut current_child = parent_item.first_child_id(parent_node_id);
5690        while let Some(child_id) = current_child {
5691            // Get content from this child (recursive)
5692            let child_content = self.get_text_before_textinput(dom_id, child_id);
5693            result.extend(child_content);
5694
5695            // Move to next sibling
5696            let child_item = match node_hierarchy.get(child_id.index()) {
5697                Some(item) => item,
5698                None => break,
5699            };
5700            current_child = child_item.next_sibling_id();
5701        }
5702
5703        result
5704    }
5705
5706    /// Extract plain text string from inline content
5707    ///
5708    /// This is a helper for building the changeset's resulting_text field.
5709    pub fn extract_text_from_inline_content(&self, content: &[InlineContent]) -> String {
5710        let mut result = String::new();
5711
5712        for item in content {
5713            match item {
5714                InlineContent::Text(text_run) => {
5715                    result.push_str(&text_run.text);
5716                }
5717                InlineContent::Space(_) => {
5718                    result.push(' ');
5719                }
5720                InlineContent::LineBreak(_) => {
5721                    result.push('\n');
5722                }
5723                InlineContent::Tab { .. } => {
5724                    result.push('\t');
5725                }
5726                InlineContent::Ruby { base, .. } => {
5727                    // For Ruby annotations, include the base text
5728                    result.push_str(&self.extract_text_from_inline_content(base));
5729                }
5730                InlineContent::Marker { run, .. } => {
5731                    // Markers contribute their text
5732                    result.push_str(&run.text);
5733                }
5734                // Images and shapes don't contribute to plain text
5735                InlineContent::Image(_) | InlineContent::Shape(_) => {}
5736            }
5737        }
5738
5739        result
5740    }
5741
5742    /// Update the text cache after a text edit
5743    ///
5744    /// This is the ONLY place where we mutate the text cache.
5745    /// All other functions are pure queries or transformations.
5746    ///
5747    /// This function:
5748    /// 1. Stores the new content in `dirty_text_nodes` for tracking
5749    /// 2. Re-runs the text3 layout pipeline (create_logical_items -> reorder -> shape -> fragment)
5750    /// 3. Updates the inline_layout_result on the IFC root node in the layout tree
5751    pub fn update_text_cache_after_edit(
5752        &mut self,
5753        dom_id: DomId,
5754        node_id: NodeId,
5755        new_inline_content: Vec<InlineContent>,
5756    ) {
5757        use crate::solver3::layout_tree::CachedInlineLayout;
5758
5759        // 1. Store the new content in dirty_text_nodes for tracking
5760        let cursor = self.cursor_manager.get_cursor().cloned();
5761        self.dirty_text_nodes.insert(
5762            (dom_id, node_id),
5763            DirtyTextNode {
5764                content: new_inline_content.clone(),
5765                cursor,
5766                needs_ancestor_relayout: false, // Will be set if size changes
5767            },
5768        );
5769
5770        // 2. Get the cached constraints from the existing inline layout result
5771        // We need to find the IFC root node and extract its constraints
5772        let constraints = {
5773            let layout_result = match self.layout_results.get(&dom_id) {
5774                Some(r) => r,
5775                None => {
5776                    return;
5777                }
5778            };
5779            
5780            let layout_node = match layout_result.layout_tree.get(node_id.index()) {
5781                Some(n) => n,
5782                None => {
5783                    return;
5784                }
5785            };
5786            
5787            let cached_layout = match &layout_node.inline_layout_result {
5788                Some(c) => c,
5789                None => {
5790                    return;
5791                }
5792            };
5793            
5794            match &cached_layout.constraints {
5795                Some(c) => c.clone(),
5796                None => {
5797                    return;
5798                }
5799            }
5800        };
5801
5802        // 3. Re-run the text3 layout pipeline
5803        let new_layout = self.relayout_text_node_internal(&new_inline_content, &constraints);
5804
5805        let Some(new_layout) = new_layout else {
5806            return;
5807        };
5808
5809        // 4. Update the layout cache with the new layout
5810        // Find the IFC root node in the layout tree and update its inline_layout_result
5811        if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
5812            // Find the node in the layout tree
5813            if let Some(layout_node) = layout_result.layout_tree.get_mut(node_id.index()) {
5814                // Check if size changed (needs repaint)
5815                let old_size = layout_node.used_size;
5816                let new_bounds = new_layout.bounds();
5817                let new_size = Some(LogicalSize {
5818                    width: new_bounds.width,
5819                    height: new_bounds.height,
5820                });
5821
5822                // Check if we need to propagate layout shift
5823                if let (Some(old), Some(new)) = (old_size, new_size) {
5824                    if (old.height - new.height).abs() > 0.5 || (old.width - new.width).abs() > 0.5 {
5825                        // Mark that ancestor relayout is needed
5826                        if let Some(dirty_node) = self.dirty_text_nodes.get_mut(&(dom_id, node_id)) {
5827                            dirty_node.needs_ancestor_relayout = true;
5828                        }
5829                    }
5830                }
5831
5832                // Update the inline layout result with the new layout but preserve constraints
5833                layout_node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
5834                    Arc::new(new_layout),
5835                    constraints.available_width,
5836                    false, // No floats in quick relayout
5837                    constraints,
5838                ));
5839            }
5840        }
5841
5842        // CRITICAL: Regenerate the display list after updating the inline layout.
5843        // Without this, the old display list (with old text glyphs) is sent to WebRender,
5844        // so the screen still shows the old text even though the layout tree is updated.
5845        self.regenerate_display_list_for_dom(dom_id);
5846    }
5847
5848    /// Regenerate the display list for a specific DOM from the current layout tree.
5849    ///
5850    /// This is the critical missing piece for text input: after `update_text_cache_after_edit`
5851    /// updates the `inline_layout_result` on layout tree nodes, the `DomLayoutResult.display_list`
5852    /// must be regenerated. Otherwise, `generate_frame()` sends the OLD display list to WebRender
5853    /// and the screen shows stale text.
5854    ///
5855    /// This method creates a temporary `LayoutContext` from the existing `LayoutWindow` state
5856    /// and calls `generate_display_list` on the already-computed layout tree and positions.
5857    fn regenerate_display_list_for_dom(&mut self, dom_id: DomId) {
5858        use crate::solver3::{
5859            display_list::generate_display_list,
5860            LayoutContext,
5861        };
5862
5863        // Get all the data we need from the layout result
5864        let layout_result = match self.layout_results.get(&dom_id) {
5865            Some(lr) => lr,
5866            None => { return; }
5867        };
5868
5869        let tree = &layout_result.layout_tree;
5870        let calculated_positions = &layout_result.calculated_positions;
5871        let scroll_ids = &layout_result.scroll_ids;
5872        let styled_dom = &layout_result.styled_dom;
5873        let viewport = layout_result.viewport;
5874
5875        // Get scroll offsets from scroll manager
5876        let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
5877
5878        // Get GPU cache for this DOM
5879        let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id).clone();
5880
5881        // Get cursor state for display list generation
5882        let cursor_is_visible = self.cursor_manager.should_draw_cursor();
5883        let cursor_location = self.cursor_manager.get_cursor_location().and_then(|loc| {
5884            self.cursor_manager.get_cursor().map(|cursor| {
5885                (loc.dom_id, loc.node_id, cursor.clone())
5886            })
5887        });
5888
5889        // Build a temporary LayoutContext with all the state we need
5890        let mut counter_values = BTreeMap::new();
5891        let mut debug_messages: Option<Vec<LayoutDebugMessage>> = None;
5892        let cache_map = std::mem::take(&mut self.layout_cache.cache_map);
5893
5894        let mut ctx = LayoutContext {
5895            styled_dom,
5896            font_manager: &self.font_manager,
5897            selections: &self.selection_manager.selections,
5898            text_selections: &self.selection_manager.text_selections,
5899            debug_messages: &mut debug_messages,
5900            counters: &mut counter_values,
5901            viewport_size: viewport.size,
5902            fragmentation_context: None,
5903            cursor_is_visible,
5904            cursor_location,
5905            cache_map,
5906            system_style: self.system_style.clone(),
5907            get_system_time_fn: azul_core::task::GetSystemTimeCallback {
5908                cb: azul_core::task::get_system_time_libstd,
5909            },
5910        };
5911
5912        // Generate the new display list from the existing layout tree
5913        let new_display_list = generate_display_list(
5914            &mut ctx,
5915            tree,
5916            calculated_positions,
5917            &scroll_offsets,
5918            scroll_ids,
5919            Some(&gpu_cache),
5920            &self.renderer_resources,
5921            self.id_namespace,
5922            dom_id,
5923        );
5924
5925        // Restore the cache_map back to layout_cache
5926        self.layout_cache.cache_map = std::mem::take(&mut ctx.cache_map);
5927
5928        match new_display_list {
5929            Ok(display_list) => {
5930                if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
5931                    layout_result.display_list = display_list;
5932                }
5933            }
5934            Err(_e) => { }
5935        }
5936    }
5937
5938    /// Internal helper to re-run the text3 layout pipeline on new content
5939    fn relayout_text_node_internal(
5940        &self,
5941        content: &[InlineContent],
5942        constraints: &UnifiedConstraints,
5943    ) -> Option<UnifiedLayout> {
5944        use crate::text3::cache::{
5945            create_logical_items, perform_fragment_layout, reorder_logical_items,
5946            shape_visual_items, BidiDirection, BreakCursor,
5947        };
5948
5949        // Stage 1: Create logical items from InlineContent
5950        let logical_items = create_logical_items(content, &[], &mut None);
5951
5952        if logical_items.is_empty() {
5953            // Empty text - return empty layout
5954            return Some(UnifiedLayout {
5955                items: Vec::new(),
5956                overflow: crate::text3::cache::OverflowInfo::default(),
5957            });
5958        }
5959
5960        // Stage 2: Bidi reordering
5961        let base_direction = constraints.direction.unwrap_or(BidiDirection::Ltr);
5962        let visual_items = reorder_logical_items(&logical_items, base_direction, &mut None).ok()?;
5963
5964        // Stage 3: Shape text (resolve fonts, create glyphs)
5965        let loaded_fonts = self.font_manager.get_loaded_fonts();
5966        let shaped_items = shape_visual_items(
5967            &visual_items,
5968            self.font_manager.get_font_chain_cache(),
5969            &self.font_manager.fc_cache,
5970            &loaded_fonts,
5971            &mut None,
5972        )
5973        .ok()?;
5974
5975        // Stage 4: Fragment layout (line breaking, positioning)
5976        let mut cursor = BreakCursor::new(&shaped_items);
5977        perform_fragment_layout(&mut cursor, &logical_items, constraints, &mut None, &loaded_fonts)
5978            .ok()
5979    }
5980
5981    /// Helper to get node used_size for accessibility actions
5982    #[cfg(feature = "a11y")]
5983    fn get_node_used_size_a11y(
5984        &self,
5985        dom_id: DomId,
5986        node_id: NodeId,
5987    ) -> Option<azul_core::geom::LogicalSize> {
5988        let layout_result = self.layout_results.get(&dom_id)?;
5989        let node = layout_result.layout_tree.get(node_id.index())?;
5990        node.used_size
5991    }
5992
5993    /// Get the layout bounds (position and size) of a specific node
5994    fn get_node_bounds(
5995        &self,
5996        dom_id: DomId,
5997        node_id: NodeId,
5998    ) -> Option<azul_css::props::basic::LayoutRect> {
5999        use azul_css::props::basic::LayoutRect;
6000
6001        let layout_result = self.layout_results.get(&dom_id)?;
6002        let node = layout_result.layout_tree.get(node_id.index())?;
6003
6004        // Get size from used_size
6005        let size = node.used_size?;
6006
6007        // Get position from calculated_positions map
6008        let position = layout_result.calculated_positions.get(node_id.index())?;
6009
6010        Some(LayoutRect {
6011            origin: azul_css::props::basic::LayoutPoint {
6012                x: position.x as f32 as isize,
6013                y: position.y as f32 as isize,
6014            },
6015            size: azul_css::props::basic::LayoutSize {
6016                width: size.width as isize,
6017                height: size.height as isize,
6018            },
6019        })
6020    }
6021
6022    /// Scroll a node into view if it's not currently visible in the viewport
6023    #[cfg(feature = "a11y")]
6024    fn scroll_to_node_if_needed(
6025        &mut self,
6026        dom_id: DomId,
6027        node_id: NodeId,
6028        now: std::time::Instant,
6029    ) {
6030        // TODO: This should:
6031        // 1. Check if node is currently visible in viewport
6032        // 2. Find the nearest scrollable ancestor
6033        // 3. Calculate the scroll offset needed to make the node visible
6034        // 4. Animate the scroll
6035
6036        // For now, just ensure the node's scroll state is at origin
6037        if self.get_node_bounds(dom_id, node_id).is_some() {
6038            self.scroll_manager.scroll_to(
6039                dom_id,
6040                node_id,
6041                LogicalPosition { x: 0.0, y: 0.0 },
6042                std::time::Duration::from_millis(300).into(),
6043                azul_core::events::EasingFunction::EaseOut,
6044                now.into(),
6045            );
6046        }
6047    }
6048
6049    /// Scroll the cursor into view if it's not currently visible
6050    ///
6051    /// This is automatically called when:
6052    /// - Focus lands on a contenteditable element
6053    /// - Cursor is moved programmatically
6054    /// - Text is inserted/deleted
6055    ///
6056    /// The function:
6057    /// 1. Gets the cursor rectangle from the text layout
6058    /// 2. Checks if the cursor is visible in the current viewport
6059    /// 3. If not, calculates the minimum scroll offset needed
6060    /// 4. Animates the scroll to bring the cursor into view
6061    fn scroll_cursor_into_view_if_needed(
6062        &mut self,
6063        dom_id: DomId,
6064        node_id: NodeId,
6065        now: std::time::Instant,
6066    ) {
6067        // Get the cursor from CursorManager
6068        let Some(cursor) = self.cursor_manager.get_cursor() else {
6069            return;
6070        };
6071
6072        // Get the inline layout for this node
6073        let Some(inline_layout) = self.get_node_inline_layout(dom_id, node_id) else {
6074            return;
6075        };
6076
6077        // Get the cursor rectangle from the text layout
6078        let Some(cursor_rect) = inline_layout.get_cursor_rect(cursor) else {
6079            return;
6080        };
6081
6082        // Get the node bounds
6083        let Some(node_bounds) = self.get_node_bounds(dom_id, node_id) else {
6084            return;
6085        };
6086
6087        // Calculate the cursor's absolute position
6088        let cursor_abs_x = node_bounds.origin.x as f32 + cursor_rect.origin.x;
6089        let cursor_abs_y = node_bounds.origin.y as f32 + cursor_rect.origin.y;
6090
6091        // Find the nearest scrollable ancestor
6092        // For now, just use the node itself if it's scrollable
6093        // TODO: Walk up the DOM tree to find scrollable ancestor
6094
6095        // Get current scroll position
6096        let current_scroll = self
6097            .scroll_manager
6098            .get_current_offset(dom_id, node_id)
6099            .unwrap_or_default();
6100
6101        // Calculate visible viewport
6102        let viewport_x = node_bounds.origin.x as f32 + current_scroll.x;
6103        let viewport_y = node_bounds.origin.y as f32 + current_scroll.y;
6104        let viewport_width = node_bounds.size.width as f32;
6105        let viewport_height = node_bounds.size.height as f32;
6106
6107        // Check if cursor is visible
6108        let cursor_visible_x = (cursor_abs_x as f32) >= viewport_x
6109            && (cursor_abs_x as f32) <= viewport_x + viewport_width;
6110        let cursor_visible_y = (cursor_abs_y as f32) >= viewport_y
6111            && (cursor_abs_y as f32) <= viewport_y + viewport_height;
6112
6113        if cursor_visible_x && cursor_visible_y {
6114            // Cursor is already visible
6115            return;
6116        }
6117
6118        // Calculate scroll offset to make cursor visible
6119        let mut target_scroll_x = current_scroll.x;
6120        let mut target_scroll_y = current_scroll.y;
6121
6122        // Adjust horizontal scroll if needed
6123        if (cursor_abs_x as f32) < viewport_x {
6124            // Cursor is to the left of viewport - scroll left
6125            target_scroll_x = cursor_abs_x as f32 - node_bounds.origin.x as f32;
6126        } else if (cursor_abs_x as f32) > viewport_x + viewport_width {
6127            // Cursor is to the right of viewport - scroll right
6128            target_scroll_x = cursor_abs_x as f32 - node_bounds.origin.x as f32 - viewport_width
6129                + cursor_rect.size.width;
6130        }
6131
6132        // Adjust vertical scroll if needed
6133        if (cursor_abs_y as f32) < viewport_y {
6134            // Cursor is above viewport - scroll up
6135            target_scroll_y = cursor_abs_y as f32 - node_bounds.origin.y as f32;
6136        } else if (cursor_abs_y as f32) > viewport_y + viewport_height {
6137            // Cursor is below viewport - scroll down
6138            target_scroll_y = cursor_abs_y as f32 - node_bounds.origin.y as f32 - viewport_height
6139                + cursor_rect.size.height;
6140        }
6141
6142        // Animate scroll to bring cursor into view
6143        self.scroll_manager.scroll_to(
6144            dom_id,
6145            node_id,
6146            LogicalPosition {
6147                x: target_scroll_x,
6148                y: target_scroll_y,
6149            },
6150            std::time::Duration::from_millis(200).into(),
6151            azul_core::events::EasingFunction::EaseOut,
6152            now.into(),
6153        );
6154    }
6155
6156    /// Convert a byte offset in the text to a TextCursor position
6157    ///
6158    /// This is used for accessibility SetTextSelection action, which provides
6159    /// byte offsets rather than grapheme cluster IDs.
6160    ///
6161    /// # Arguments
6162    ///
6163    /// * `text_layout` - The text layout containing the shaped runs
6164    /// * `byte_offset` - The byte offset in the UTF-8 text
6165    ///
6166    /// # Returns
6167    ///
6168    /// A TextCursor positioned at the given byte offset, or None if the offset
6169    /// is out of bounds.
6170    fn byte_offset_to_cursor(
6171        &self,
6172        text_layout: &UnifiedLayout,
6173        byte_offset: u32,
6174    ) -> Option<TextCursor> {
6175        // Handle offset 0 as special case (start of text)
6176        if byte_offset == 0 {
6177            // Find first cluster in items
6178            for item in &text_layout.items {
6179                if let ShapedItem::Cluster(cluster) = &item.item {
6180                    return Some(TextCursor {
6181                        cluster_id: cluster.source_cluster_id,
6182                        affinity: CursorAffinity::Trailing,
6183                    });
6184                }
6185            }
6186            // No clusters found - return default
6187            return Some(TextCursor {
6188                cluster_id: GraphemeClusterId {
6189                    source_run: 0,
6190                    start_byte_in_run: 0,
6191                },
6192                affinity: CursorAffinity::Trailing,
6193            });
6194        }
6195
6196        // Iterate through items to find which cluster contains this byte offset
6197        let mut current_byte_offset = 0u32;
6198
6199        for item in &text_layout.items {
6200            if let ShapedItem::Cluster(cluster) = &item.item {
6201                // Calculate byte length of this cluster from its text
6202                let cluster_byte_length = cluster.text.len() as u32;
6203                let cluster_end_byte = current_byte_offset + cluster_byte_length;
6204
6205                // Check if our target byte offset falls within this cluster
6206                if byte_offset >= current_byte_offset && byte_offset <= cluster_end_byte {
6207                    // Found the cluster
6208                    return Some(TextCursor {
6209                        cluster_id: cluster.source_cluster_id,
6210                        affinity: CursorAffinity::Trailing,
6211                    });
6212                }
6213
6214                current_byte_offset = cluster_end_byte;
6215            }
6216        }
6217
6218        // Offset is beyond the end of all text - return cursor at end of last cluster
6219        for item in text_layout.items.iter().rev() {
6220            if let ShapedItem::Cluster(cluster) = &item.item {
6221                return Some(TextCursor {
6222                    cluster_id: cluster.source_cluster_id,
6223                    affinity: CursorAffinity::Trailing,
6224                });
6225            }
6226        }
6227
6228        // No clusters at all - return default position
6229        Some(TextCursor {
6230            cluster_id: GraphemeClusterId {
6231                source_run: 0,
6232                start_byte_in_run: 0,
6233            },
6234            affinity: CursorAffinity::Trailing,
6235        })
6236    }
6237
6238    /// Get the inline layout result for a specific node
6239    ///
6240    /// This looks up the node in the layout tree and returns its inline layout result
6241    /// if it exists.
6242    fn get_node_inline_layout(
6243        &self,
6244        dom_id: DomId,
6245        node_id: NodeId,
6246    ) -> Option<alloc::sync::Arc<UnifiedLayout>> {
6247        // Get the layout tree from cache
6248        let layout_tree = self.layout_cache.tree.as_ref()?;
6249
6250        // Find the layout node corresponding to the DOM node
6251        let layout_node = layout_tree
6252            .nodes
6253            .iter()
6254            .find(|node| node.dom_node_id == Some(node_id))?;
6255
6256        // Return the inline layout result
6257        layout_node
6258            .inline_layout_result
6259            .as_ref()
6260            .map(|c| c.clone_layout())
6261    }
6262
6263    /// Sync cursor from CursorManager to SelectionManager for rendering
6264    ///
6265    /// The renderer expects cursor and selection data from the SelectionManager,
6266    /// but we manage the cursor separately in the CursorManager for better separation
6267    /// of concerns. This function syncs the cursor state so it can be rendered.
6268    ///
6269    /// This should be called whenever the cursor changes.
6270    pub fn sync_cursor_to_selection_manager(&mut self) {
6271        if let Some(cursor) = self.cursor_manager.get_cursor() {
6272            if let Some(location) = self.cursor_manager.get_cursor_location() {
6273                // Convert cursor to Selection
6274                let selection = Selection::Cursor(cursor.clone());
6275
6276                // Create SelectionState
6277                let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(location.node_id));
6278                let dom_node_id = DomNodeId {
6279                    dom: location.dom_id,
6280                    node: hierarchy_id,
6281                };
6282
6283                let selection_state = SelectionState {
6284                    selections: vec![selection].into(),
6285                    node_id: dom_node_id,
6286                };
6287
6288                // Set selection in SelectionManager
6289                self.selection_manager
6290                    .set_selection(location.dom_id, selection_state);
6291            }
6292        }
6293        // NOTE: We intentionally do NOT clear selections when there's no cursor.
6294        // Text selections from mouse clicks should persist independently of cursor state.
6295        // Only explicit user actions (clicking elsewhere, Escape, etc.) should clear selections.
6296    }
6297
6298    /// Edit the text content of a node (used for text input actions)
6299    ///
6300    /// This function applies text edits to nodes that contain text content.
6301    /// The DOM node itself is NOT modified - instead, the text cache is updated
6302    /// with the new shaped text that reflects the edit, cursor, and selection.
6303    ///
6304    /// It handles:
6305    /// - ReplaceSelectedText: Replaces the current selection with new text
6306    /// - SetValue: Sets the entire text value
6307    /// - SetNumericValue: Converts number to string and sets value
6308    ///
6309    /// # Returns
6310    ///
6311    /// Returns a Vec of DomNodeIds (node + parent) that need to be marked dirty
6312    /// for re-layout. The caller MUST use this return value to trigger layout.
6313    #[must_use = "Returned nodes must be marked dirty for re-layout"]
6314    #[cfg(feature = "a11y")]
6315    pub fn edit_text_node(
6316        &mut self,
6317        dom_id: DomId,
6318        node_id: NodeId,
6319        edit_type: TextEditType,
6320    ) -> Vec<azul_core::dom::DomNodeId> {
6321        use crate::managers::text_input::TextInputSource;
6322
6323        // Convert TextEditType to string
6324        let text_input = match &edit_type {
6325            TextEditType::ReplaceSelection(text) => text.clone(),
6326            TextEditType::SetValue(text) => text.clone(),
6327            TextEditType::SetNumericValue(value) => value.to_string(),
6328        };
6329
6330        // Get the OLD text before any changes
6331        let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
6332        let old_text = self.extract_text_from_inline_content(&old_inline_content);
6333
6334        // Create DomNodeId
6335        let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
6336        let dom_node_id = azul_core::dom::DomNodeId {
6337            dom: dom_id,
6338            node: hierarchy_id,
6339        };
6340
6341        // Record the changeset in TextInputManager
6342        self.text_input_manager.record_input(
6343            dom_node_id,
6344            text_input,
6345            old_text,
6346            TextInputSource::Accessibility, // A11y source
6347        );
6348
6349        // Immediately apply the changeset (A11y doesn't go through callbacks)
6350        self.apply_text_changeset()
6351    }
6352
6353    #[cfg(not(feature = "a11y"))]
6354    pub fn process_accessibility_action(
6355        &mut self,
6356        _dom_id: DomId,
6357        _node_id: NodeId,
6358        _action: azul_core::dom::AccessibilityAction,
6359        _now: std::time::Instant,
6360    ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
6361        // No-op when accessibility is disabled
6362        BTreeMap::new()
6363    }
6364
6365    /// Process mouse click for text selection.
6366    ///
6367    /// This method handles:
6368    /// - Single click: Place cursor at click position
6369    /// - Double click: Select word at click position
6370    /// - Triple click: Select paragraph (line) at click position
6371    ///
6372    /// ## Workflow
6373    /// 1. Use HoverManager's hit test to find hit nodes
6374    /// 2. Find the IFC layout via `inline_layout_result` (IFC root) or `ifc_membership` (text node)
6375    /// 3. Use point_relative_to_item for local cursor position
6376    /// 4. Hit-test the text layout to get logical cursor
6377    /// 5. Apply appropriate selection based on click count
6378    /// 6. Update SelectionManager with new selection
6379    ///
6380    /// ## IFC Architecture
6381    /// Text nodes don't store `inline_layout_result` directly. Instead:
6382    /// - IFC root nodes (e.g., `<p>`) have `inline_layout_result` with the complete text layout
6383    /// - Text nodes have `ifc_membership` pointing back to their IFC root
6384    /// - This allows efficient lookup without iterating all nodes
6385    ///
6386    /// ## Parameters
6387    /// * `position` - Click position in logical coordinates (for click count tracking)
6388    /// * `time_ms` - Current time in milliseconds (for multi-click detection)
6389    ///
6390    /// ## Returns
6391    /// * `Option<Vec<DomNodeId>>` - Affected nodes that need re-rendering, None if click didn't hit text
6392    pub fn process_mouse_click_for_selection(
6393        &mut self,
6394        position: azul_core::geom::LogicalPosition,
6395        time_ms: u64,
6396    ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6397        use crate::managers::hover::InputPointId;
6398        use crate::text3::selection::{select_paragraph_at_cursor, select_word_at_cursor};
6399
6400        // found_selection stores: (dom_id, ifc_root_node_id, selection_range, local_pos)
6401        // IMPORTANT: We always store the IFC root NodeId, not the text node NodeId,
6402        // because selections are rendered via inline_layout_result which lives on the IFC root.
6403        let mut found_selection: Option<(DomId, NodeId, SelectionRange, azul_core::geom::LogicalPosition)> = None;
6404
6405        // Try to get hit test from HoverManager first (fast path, uses WebRender's point_relative_to_item)
6406        if let Some(hit_test) = self.hover_manager.get_current(&InputPointId::Mouse) {
6407            // Iterate through hit nodes from the HoverManager
6408            for (dom_id, hit) in &hit_test.hovered_nodes {
6409                let layout_result = match self.layout_results.get(dom_id) {
6410                    Some(lr) => lr,
6411                    None => continue,
6412                };
6413                // Use layout tree from layout_result, not layout_cache
6414                let tree = &layout_result.layout_tree;
6415                
6416                // Sort by DOM depth (deepest first) to prefer specific text nodes over containers.
6417                // We count the actual number of parents to determine DOM depth properly.
6418                // Secondary sort by NodeId for deterministic ordering within the same depth.
6419                let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
6420                let get_dom_depth = |node_id: &NodeId| -> usize {
6421                    let mut depth = 0;
6422                    let mut current = *node_id;
6423                    while let Some(parent) = node_hierarchy.get(current).and_then(|h| h.parent_id()) {
6424                        depth += 1;
6425                        current = parent;
6426                    }
6427                    depth
6428                };
6429                
6430                let mut sorted_hits: Vec<_> = hit.regular_hit_test_nodes.iter().collect();
6431                sorted_hits.sort_by(|(a_id, _), (b_id, _)| {
6432                    let depth_a = get_dom_depth(a_id);
6433                    let depth_b = get_dom_depth(b_id);
6434                    // Higher depth = deeper in DOM = should come first
6435                    // Then sort by NodeId for deterministic order within same depth
6436                    depth_b.cmp(&depth_a).then_with(|| a_id.index().cmp(&b_id.index()))
6437                });
6438                
6439                for (node_id, hit_item) in sorted_hits {
6440                    // Check if text is selectable
6441                    if !self.is_text_selectable(&layout_result.styled_dom, *node_id) {
6442                        continue;
6443                    }
6444                    
6445                    // Find the layout node for this DOM node
6446                    let layout_node_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(*node_id));
6447                    let layout_node_idx = match layout_node_idx {
6448                        Some(idx) => idx,
6449                        None => continue,
6450                    };
6451                    let layout_node = &tree.nodes[layout_node_idx];
6452                    
6453                    // Get the IFC layout and IFC root NodeId
6454                    // Selection must be stored on the IFC root, not on text nodes
6455                    let (cached_layout, ifc_root_node_id) = if let Some(ref cached) = layout_node.inline_layout_result {
6456                        // This node IS an IFC root - use its own NodeId
6457                        (cached, *node_id)
6458                    } else if let Some(ref membership) = layout_node.ifc_membership {
6459                        // This node participates in an IFC - get layout and NodeId from IFC root
6460                        match tree.nodes.get(membership.ifc_root_layout_index) {
6461                            Some(ifc_root) => match (ifc_root.inline_layout_result.as_ref(), ifc_root.dom_node_id) {
6462                                (Some(cached), Some(root_dom_id)) => (cached, root_dom_id),
6463                                _ => continue,
6464                            },
6465                            None => continue,
6466                        }
6467                    } else {
6468                        // No IFC involvement - not a text node
6469                        continue;
6470                    };
6471                    
6472                    let layout = &cached_layout.layout;
6473                    
6474                    // Use point_relative_to_item - this is the local position within the hit node
6475                    // provided by WebRender's hit test
6476                    let local_pos = hit_item.point_relative_to_item;
6477                    
6478                    // Hit-test the cursor in this text layout
6479                    if let Some(cursor) = layout.hittest_cursor(local_pos) {
6480                        // Store selection with IFC root NodeId, not the hit text node
6481                        found_selection = Some((*dom_id, ifc_root_node_id, SelectionRange {
6482                            start: cursor.clone(),
6483                            end: cursor,
6484                        }, local_pos));
6485                        break;
6486                    }
6487                }
6488                
6489                if found_selection.is_some() {
6490                    break;
6491                }
6492            }
6493        }
6494
6495        // Fallback: If HoverManager has no hit test (e.g., debug server),
6496        // search through IFC roots using global position
6497        if found_selection.is_none() {
6498            for (dom_id, layout_result) in &self.layout_results {
6499                // Use the layout tree from layout_result, not layout_cache
6500                // layout_cache.tree is for the root DOM only; layout_result.layout_tree
6501                // is the correct tree for each DOM (including iframes)
6502                let tree = &layout_result.layout_tree;
6503                
6504                // Only iterate IFC roots (nodes with inline_layout_result)
6505                for (node_idx, layout_node) in tree.nodes.iter().enumerate() {
6506                    let cached_layout = match layout_node.inline_layout_result.as_ref() {
6507                        Some(c) => c,
6508                        None => continue, // Skip non-IFC-root nodes
6509                    };
6510                    
6511                    let node_id = match layout_node.dom_node_id {
6512                        Some(n) => n,
6513                        None => continue,
6514                    };
6515                    
6516                    // Check if text is selectable
6517                    if !self.is_text_selectable(&layout_result.styled_dom, node_id) {
6518                        continue;
6519                    }
6520                    
6521                    // Get the node's absolute position
6522                    // Use layout_result.calculated_positions for the correct DOM
6523                    let node_pos = layout_result.calculated_positions
6524            .get(node_idx)
6525                        .copied()
6526                        .unwrap_or_default();
6527                    
6528                    // Check if position is within node bounds
6529                    let node_size = layout_node.used_size.unwrap_or_else(|| {
6530                        let bounds = cached_layout.layout.bounds();
6531                        azul_core::geom::LogicalSize::new(bounds.width, bounds.height)
6532                    });
6533                    
6534                    if position.x < node_pos.x || position.x > node_pos.x + node_size.width ||
6535                       position.y < node_pos.y || position.y > node_pos.y + node_size.height {
6536                        continue;
6537                    }
6538                    
6539                    // Convert global position to node-local coordinates
6540                    let local_pos = azul_core::geom::LogicalPosition {
6541                        x: position.x - node_pos.x,
6542                        y: position.y - node_pos.y,
6543                    };
6544                    
6545                    let layout = &cached_layout.layout;
6546                    
6547                    // Hit-test the cursor in this text layout
6548                    if let Some(cursor) = layout.hittest_cursor(local_pos) {
6549                        found_selection = Some((*dom_id, node_id, SelectionRange {
6550                            start: cursor.clone(),
6551                            end: cursor,
6552                        }, local_pos));
6553                        break;
6554                    }
6555                }
6556                
6557                if found_selection.is_some() {
6558                    break;
6559                }
6560            }
6561        }
6562
6563        let (dom_id, ifc_root_node_id, initial_range, _local_pos) = found_selection?;
6564
6565        // Create DomNodeId for click state tracking - use IFC root's NodeId
6566        // Selection state is keyed by IFC root because that's where inline_layout_result lives
6567        let node_hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(ifc_root_node_id));
6568        let dom_node_id = azul_core::dom::DomNodeId {
6569            dom: dom_id,
6570            node: node_hierarchy_id,
6571        };
6572
6573        // Update click count to determine selection type
6574        let click_count = self
6575            .selection_manager
6576            .update_click_count(dom_node_id, position, time_ms);
6577
6578        // Get the text layout again for word/paragraph selection
6579        let final_range = if click_count > 1 {
6580            // Use layout_results for the correct DOM's tree
6581            let layout_result = self.layout_results.get(&dom_id)?;
6582            let tree = &layout_result.layout_tree;
6583            
6584            // Find layout node - ifc_root_node_id is always the IFC root, so it has inline_layout_result
6585            let layout_node = tree.nodes.iter().find(|n| n.dom_node_id == Some(ifc_root_node_id))?;
6586            let cached_layout = layout_node.inline_layout_result.as_ref()?;
6587            let layout = &cached_layout.layout;
6588            
6589            match click_count {
6590                2 => select_word_at_cursor(&initial_range.start, layout.as_ref())
6591                    .unwrap_or(initial_range),
6592                3 => select_paragraph_at_cursor(&initial_range.start, layout.as_ref())
6593                    .unwrap_or(initial_range),
6594                _ => initial_range,
6595            }
6596        } else {
6597            initial_range
6598        };
6599
6600        // Clear existing selections and set new one using the NEW anchor/focus model
6601        // First, get the cursor bounds for the anchor
6602        let char_bounds = {
6603            let layout_result = self.layout_results.get(&dom_id)?;
6604            let tree = &layout_result.layout_tree;
6605            let layout_node = tree.nodes.iter().find(|n| n.dom_node_id == Some(ifc_root_node_id))?;
6606            let cached_layout = layout_node.inline_layout_result.as_ref()?;
6607            cached_layout.layout.get_cursor_rect(&final_range.start)
6608                .unwrap_or(azul_core::geom::LogicalRect {
6609                    origin: position,
6610                    size: azul_core::geom::LogicalSize { width: 1.0, height: 16.0 },
6611                })
6612        };
6613        
6614        // Clear any existing text selection for this DOM
6615        self.selection_manager.clear_text_selection(&dom_id);
6616        
6617        // Start a new selection with the anchor at the clicked position
6618        self.selection_manager.start_selection(
6619            dom_id,
6620            ifc_root_node_id,
6621            final_range.start,
6622            char_bounds,
6623            position,
6624        );
6625        
6626        // Also update the legacy selection state for backward compatibility with rendering
6627        self.selection_manager.clear_selection(&dom_id);
6628
6629        let state = SelectionState {
6630            selections: vec![Selection::Range(final_range)].into(),
6631            node_id: dom_node_id,
6632        };
6633        
6634        self.selection_manager.set_selection(dom_id, state);
6635
6636        // CRITICAL FIX 1: Set focus on the clicked node
6637        // Without this, clicking on a contenteditable element shows a cursor but
6638        // text input doesn't work because record_text_input() checks focus_manager.get_focused_node()
6639        // and returns early if there's no focus.
6640        //
6641        // Check if the node OR ANY ANCESTOR is contenteditable before setting focus
6642        // The contenteditable attribute is typically on a parent div, not on the IFC root or text node
6643        let is_contenteditable = self.layout_results.get(&dom_id)
6644            .map(|lr| {
6645                let node_hierarchy = lr.styled_dom.node_hierarchy.as_container();
6646                let node_data = lr.styled_dom.node_data.as_ref();
6647                
6648                // Walk up the DOM tree to check if any ancestor has contenteditable
6649                let mut current_node = Some(ifc_root_node_id);
6650                while let Some(node_id) = current_node {
6651                    if let Some(styled_node) = node_data.get(node_id.index()) {
6652                        // Check BOTH: the contenteditable boolean field AND the attribute
6653                        // NodeData has a direct `contenteditable: bool` field that should be
6654                        // checked in addition to the attribute for robustness
6655                        if styled_node.contenteditable {
6656                            return true;
6657                        }
6658                        
6659                        // Also check the attribute (for backwards compatibility)
6660                        let has_contenteditable_attr = styled_node.attributes.as_ref().iter().any(|attr| {
6661                            matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
6662                        });
6663                        if has_contenteditable_attr {
6664                            return true;
6665                        }
6666                    }
6667                    // Move to parent
6668                    current_node = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
6669                }
6670                false
6671            })
6672            .unwrap_or(false);
6673        
6674        // W3C conformance: contenteditable elements are implicitly focusable
6675        if is_contenteditable {
6676            self.focus_manager.set_focused_node(Some(dom_node_id));
6677        }
6678
6679        // CRITICAL FIX 2: Initialize the CursorManager with the clicked position
6680        // Without this, clicking on a contenteditable element sets focus (blue outline)
6681        // but the text cursor doesn't appear because CursorManager is never told where to draw it.
6682        let now = azul_core::task::Instant::now();
6683        self.cursor_manager.move_cursor_to(
6684            final_range.start.clone(),
6685            dom_id,
6686            ifc_root_node_id,
6687        );
6688        // Reset the blink timer so the cursor is immediately visible
6689        self.cursor_manager.reset_blink_on_input(now);
6690        self.cursor_manager.set_blink_timer_active(true);
6691
6692        // Return the affected node for dirty tracking
6693        Some(vec![dom_node_id])
6694    }
6695
6696    /// Process mouse drag for text selection extension.
6697    ///
6698    /// This method handles drag-to-select by extending the selection from
6699    /// the anchor (mousedown position) to the current focus (drag position).
6700    ///
6701    /// Uses the anchor/focus model:
6702    /// - Anchor is fixed at the initial click position (set by process_mouse_click_for_selection)
6703    /// - Focus moves with the mouse during drag
6704    /// - Affected nodes between anchor and focus are computed in DOM order
6705    ///
6706    /// ## Parameters
6707    /// * `start_position` - Initial click position in logical coordinates (unused, anchor is stored)
6708    /// * `current_position` - Current mouse position in logical coordinates
6709    ///
6710    /// ## Returns
6711    /// * `Option<Vec<DomNodeId>>` - Affected nodes that need re-rendering
6712    pub fn process_mouse_drag_for_selection(
6713        &mut self,
6714        _start_position: azul_core::geom::LogicalPosition,
6715        current_position: azul_core::geom::LogicalPosition,
6716    ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6717        use crate::managers::hover::InputPointId;
6718
6719        // Find which DOM has an active text selection with an anchor
6720        let dom_id = self.selection_manager.get_all_text_selections()
6721            .keys()
6722            .next()
6723            .copied()?;
6724        
6725        // Get the existing anchor from the text selection
6726        let anchor = {
6727            let text_selection = self.selection_manager.get_text_selection(&dom_id)?;
6728            text_selection.anchor.clone()
6729        };
6730        
6731        // Get the current hit test from HoverManager
6732        let hit_test = self.hover_manager.get_current(&InputPointId::Mouse)?;
6733
6734        // Find the focus position (current cursor under mouse)
6735        let mut focus_info: Option<(NodeId, TextCursor, azul_core::geom::LogicalPosition)> = None;
6736        
6737        for (hit_dom_id, hit) in &hit_test.hovered_nodes {
6738            if *hit_dom_id != dom_id {
6739                continue;
6740            }
6741            
6742            let layout_result = match self.layout_results.get(hit_dom_id) {
6743                Some(lr) => lr,
6744                None => continue,
6745            };
6746            let tree = &layout_result.layout_tree;
6747            
6748            for (node_id, hit_item) in &hit.regular_hit_test_nodes {
6749                if !self.is_text_selectable(&layout_result.styled_dom, *node_id) {
6750                    continue;
6751                }
6752
6753                let layout_node_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(*node_id));
6754                let layout_node_idx = match layout_node_idx {
6755                    Some(idx) => idx,
6756                    None => continue,
6757                };
6758                let layout_node = &tree.nodes[layout_node_idx];
6759                
6760                // Get the IFC layout and IFC root NodeId
6761                let (cached_layout, ifc_root_node_id) = if let Some(ref cached) = layout_node.inline_layout_result {
6762                    (cached, *node_id)
6763                } else if let Some(ref membership) = layout_node.ifc_membership {
6764                    match tree.nodes.get(membership.ifc_root_layout_index) {
6765                        Some(ifc_root) => match (ifc_root.inline_layout_result.as_ref(), ifc_root.dom_node_id) {
6766                            (Some(cached), Some(root_dom_id)) => (cached, root_dom_id),
6767                            _ => continue,
6768                        },
6769                        None => continue,
6770                    }
6771                } else {
6772                    continue;
6773                };
6774                
6775                let local_pos = hit_item.point_relative_to_item;
6776                
6777                if let Some(cursor) = cached_layout.layout.hittest_cursor(local_pos) {
6778                    focus_info = Some((ifc_root_node_id, cursor, current_position));
6779                    break;
6780                }
6781            }
6782            
6783            if focus_info.is_some() {
6784                break;
6785            }
6786        }
6787        
6788        let (focus_ifc_root, focus_cursor, focus_mouse_pos) = focus_info?;
6789        
6790        // Compute affected nodes between anchor and focus
6791        let layout_result = self.layout_results.get(&dom_id)?;
6792        let hierarchy = &layout_result.styled_dom.node_hierarchy;
6793        
6794        // Determine document order
6795        let is_forward = if anchor.ifc_root_node_id == focus_ifc_root {
6796            // Same IFC - compare cursors
6797            anchor.cursor <= focus_cursor
6798        } else {
6799            // Different IFCs - use document order
6800            is_before_in_document_order(hierarchy, anchor.ifc_root_node_id, focus_ifc_root)
6801        };
6802        
6803        let (start_node, end_node) = if is_forward {
6804            (anchor.ifc_root_node_id, focus_ifc_root)
6805        } else {
6806            (focus_ifc_root, anchor.ifc_root_node_id)
6807        };
6808        
6809        // Collect all IFC roots between start and end
6810        let nodes_in_range = collect_nodes_in_document_order(hierarchy, start_node, end_node);
6811        
6812        // Build the affected_nodes map with SelectionRanges for each IFC root
6813        let mut affected_nodes_map = std::collections::BTreeMap::new();
6814        let tree = &layout_result.layout_tree;
6815        
6816        for node_id in &nodes_in_range {
6817            // Check if this node is an IFC root (has inline_layout_result)
6818            let layout_node = tree.nodes.iter().find(|n| n.dom_node_id == Some(*node_id));
6819            let layout_node = match layout_node {
6820                Some(ln) if ln.inline_layout_result.is_some() => ln,
6821                _ => continue, // Skip non-IFC-root nodes
6822            };
6823            
6824            let cached_layout = layout_node.inline_layout_result.as_ref()?;
6825            let layout = &cached_layout.layout;
6826            
6827            let range = if *node_id == anchor.ifc_root_node_id && *node_id == focus_ifc_root {
6828                // Both anchor and focus in same IFC
6829                SelectionRange {
6830                    start: if is_forward { anchor.cursor } else { focus_cursor },
6831                    end: if is_forward { focus_cursor } else { anchor.cursor },
6832                }
6833            } else if *node_id == anchor.ifc_root_node_id {
6834                // Anchor node - select from anchor to end (if forward) or start to anchor (if backward)
6835                if is_forward {
6836                    let end_cursor = layout.get_last_cluster_cursor()
6837                        .unwrap_or(anchor.cursor);
6838                    SelectionRange { start: anchor.cursor, end: end_cursor }
6839                } else {
6840                    let start_cursor = layout.get_first_cluster_cursor()
6841                        .unwrap_or(anchor.cursor);
6842                    SelectionRange { start: start_cursor, end: anchor.cursor }
6843                }
6844            } else if *node_id == focus_ifc_root {
6845                // Focus node - select from start to focus (if forward) or focus to end (if backward)
6846                if is_forward {
6847                    let start_cursor = layout.get_first_cluster_cursor()
6848                        .unwrap_or(focus_cursor);
6849                    SelectionRange { start: start_cursor, end: focus_cursor }
6850                } else {
6851                    let end_cursor = layout.get_last_cluster_cursor()
6852                        .unwrap_or(focus_cursor);
6853                    SelectionRange { start: focus_cursor, end: end_cursor }
6854                }
6855            } else {
6856                // Middle node - fully selected
6857                let start_cursor = layout.get_first_cluster_cursor()?;
6858                let end_cursor = layout.get_last_cluster_cursor()?;
6859                SelectionRange { start: start_cursor, end: end_cursor }
6860            };
6861            
6862            affected_nodes_map.insert(*node_id, range);
6863        }
6864        
6865        // Update the text selection with new focus and affected nodes
6866        // This does NOT clear the anchor!
6867        self.selection_manager.update_selection_focus(
6868            &dom_id,
6869            focus_ifc_root,
6870            focus_cursor,
6871            focus_mouse_pos,
6872            affected_nodes_map.clone(),
6873            is_forward,
6874        );
6875        
6876        // Also update the legacy selection state for backward compatibility with rendering
6877        // For now, we just update the anchor's IFC root with the visible range
6878        if let Some(anchor_range) = affected_nodes_map.get(&anchor.ifc_root_node_id) {
6879            let node_hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(anchor.ifc_root_node_id));
6880            let dom_node_id = azul_core::dom::DomNodeId {
6881                dom: dom_id,
6882                node: node_hierarchy_id,
6883            };
6884            
6885            let state = SelectionState {
6886                selections: vec![Selection::Range(*anchor_range)].into(),
6887                node_id: dom_node_id,
6888            };
6889            self.selection_manager.set_selection(dom_id, state);
6890        }
6891
6892        // Return affected nodes for dirty tracking
6893        let affected_dom_nodes: Vec<azul_core::dom::DomNodeId> = affected_nodes_map.keys()
6894            .map(|node_id| azul_core::dom::DomNodeId {
6895                dom: dom_id,
6896                node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
6897            })
6898            .collect();
6899        
6900        if affected_dom_nodes.is_empty() {
6901            None
6902        } else {
6903            Some(affected_dom_nodes)
6904        }
6905    }
6906
6907    /// Delete the currently selected text
6908    ///
6909    /// Handles Backspace/Delete key when a selection exists. The selection is deleted
6910    /// and replaced with a single cursor at the deletion point.
6911    ///
6912    /// ## Arguments
6913    /// * `target` - The target node (focused contenteditable element)
6914    /// * `forward` - true for Delete key (forward), false for Backspace (backward)
6915    ///
6916    /// ## Returns
6917    /// * `Some(Vec<DomNodeId>)` - Affected nodes if selection was deleted
6918    /// * `None` - If no selection exists or deletion failed
6919    pub fn delete_selection(
6920        &mut self,
6921        target: azul_core::dom::DomNodeId,
6922        forward: bool,
6923    ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6924        let dom_id = target.dom;
6925
6926        // Get current selection ranges
6927        let ranges = self.selection_manager.get_ranges(&dom_id);
6928        if ranges.is_empty() {
6929            return None; // No selection to delete
6930        }
6931
6932        // For each selection range, delete the selected text
6933        // Note: For now, we just clear the selection and place cursor at start
6934        // Full implementation would need to modify the underlying text content
6935        // via the changeset system
6936
6937        // Find the earliest cursor position from all ranges
6938        let mut earliest_cursor = None;
6939        for range in &ranges {
6940            // Use the start position for backward deletion, end for forward
6941            let cursor = if forward { range.end } else { range.start };
6942
6943            if earliest_cursor.is_none() {
6944                earliest_cursor = Some(cursor);
6945            } else if let Some(current) = earliest_cursor {
6946                // Compare cursor positions using cluster_id ordering
6947                // Earlier cluster_id means earlier position in text
6948                if cursor < current {
6949                    earliest_cursor = Some(cursor);
6950                }
6951            }
6952        }
6953
6954        // Clear selection and place cursor at deletion point
6955        self.selection_manager.clear_selection(&dom_id);
6956
6957        if let Some(cursor) = earliest_cursor {
6958            // Set cursor at deletion point
6959            let state = SelectionState {
6960                selections: vec![Selection::Range(SelectionRange {
6961                    start: cursor.clone(),
6962                    end: cursor,
6963                })]
6964                .into(),
6965                node_id: target,
6966            };
6967            self.selection_manager.set_selection(dom_id, state);
6968        }
6969
6970        // Return affected nodes for dirty tracking
6971        Some(vec![target])
6972    }
6973
6974    /// Extract clipboard content from the current selection
6975    ///
6976    /// This method extracts both plain text and styled text from the selection ranges.
6977    /// It iterates through all selected text, extracts the actual characters, and
6978    /// preserves styling information from the ShapedGlyph's StyleProperties.
6979    ///
6980    /// This is NOT reading from the system clipboard - use `clipboard_manager.get_paste_content()`
6981    /// for that. This extracts content FROM the selection TO be copied.
6982    ///
6983    /// ## Arguments
6984    /// * `dom_id` - The DOM to extract selection from
6985    ///
6986    /// ## Returns
6987    /// * `Some(ClipboardContent)` - If there is a selection with text
6988    /// * `None` - If no selection or no text layouts found
6989    pub fn get_selected_content_for_clipboard(
6990        &self,
6991        dom_id: &DomId,
6992    ) -> Option<crate::managers::selection::ClipboardContent> {
6993        use crate::{
6994            managers::selection::{ClipboardContent, StyledTextRun},
6995            text3::cache::ShapedItem,
6996        };
6997
6998        // Get selection ranges for this DOM
6999        let ranges = self.selection_manager.get_ranges(dom_id);
7000        if ranges.is_empty() {
7001            return None;
7002        }
7003
7004        let mut plain_text = String::new();
7005        let mut styled_runs = Vec::new();
7006
7007        // Iterate through all text layouts to find selected content
7008        for cache_id in self.text_cache.get_all_layout_ids() {
7009            let layout = self.text_cache.get_layout(&cache_id)?;
7010
7011            // Process each selection range
7012            for range in &ranges {
7013                // Iterate through positioned items in the layout
7014                for positioned_item in &layout.items {
7015                    match &positioned_item.item {
7016                        ShapedItem::Cluster(cluster) => {
7017                            // Check if this cluster is within the selection range
7018                            let cluster_id = cluster.source_cluster_id;
7019
7020                            // Simple check: is this cluster between start and end?
7021                            let in_range = if range.start.cluster_id <= range.end.cluster_id {
7022                                cluster_id >= range.start.cluster_id
7023                                    && cluster_id <= range.end.cluster_id
7024                            } else {
7025                                cluster_id >= range.end.cluster_id
7026                                    && cluster_id <= range.start.cluster_id
7027                            };
7028
7029                            if in_range {
7030                                // Extract text from cluster
7031                                plain_text.push_str(&cluster.text);
7032
7033                                // Extract styling from first glyph (they share styling)
7034                                if let Some(first_glyph) = cluster.glyphs.first() {
7035                                    let style = &first_glyph.style;
7036
7037                                    // Extract font family from font stack
7038                                    let default_font = FontSelector::default();
7039                                    let first_font = style.font_stack.first_selector()
7040                                        .unwrap_or(&default_font);
7041                                    let font_family: OptionString =
7042                                        Some(AzString::from(first_font.family.as_str())).into();
7043
7044                                    // Check if bold/italic from font selector
7045                                    use rust_fontconfig::FcWeight;
7046                                    let is_bold = matches!(
7047                                        first_font.weight,
7048                                        FcWeight::Bold | FcWeight::ExtraBold | FcWeight::Black
7049                                    );
7050                                    let is_italic = matches!(
7051                                        first_font.style,
7052                                        FontStyle::Italic | FontStyle::Oblique
7053                                    );
7054
7055                                    styled_runs.push(StyledTextRun {
7056                                        text: cluster.text.clone().into(),
7057                                        font_family,
7058                                        font_size_px: style.font_size_px,
7059                                        color: style.color,
7060                                        is_bold,
7061                                        is_italic,
7062                                    });
7063                                }
7064                            }
7065                        }
7066                        // For now, skip non-cluster items (objects, breaks, etc.)
7067                        _ => {}
7068                    }
7069                }
7070            }
7071        }
7072
7073        if plain_text.is_empty() {
7074            None
7075        } else {
7076            Some(ClipboardContent {
7077                plain_text: plain_text.into(),
7078                styled_runs: styled_runs.into(),
7079            })
7080        }
7081    }
7082
7083    /// Process image callback updates from CallbackChangeResult
7084    ///
7085    /// This function re-invokes image callbacks for nodes that requested updates
7086    /// (typically from timer callbacks or resize events). It returns the updated
7087    /// textures along with their metadata for the rendering pipeline to process.
7088    ///
7089    /// # Arguments
7090    ///
7091    /// * `image_callbacks_changed` - Map of DomId -> Set of NodeIds that need re-rendering
7092    /// * `gl_context` - OpenGL context pointer for rendering
7093    ///
7094    /// # Returns
7095    ///
7096    /// Vector of (DomId, NodeId, Texture) tuples for textures that were updated
7097    pub fn process_image_callback_updates(
7098        &mut self,
7099        image_callbacks_changed: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
7100        gl_context: &OptionGlContextPtr,
7101    ) -> Vec<(DomId, NodeId, azul_core::gl::Texture)> {
7102        use crate::callbacks::{RenderImageCallback, RenderImageCallbackInfo};
7103
7104        let mut updated_textures = Vec::new();
7105
7106        for (dom_id, node_ids) in image_callbacks_changed {
7107            let layout_result = match self.layout_results.get_mut(dom_id) {
7108                Some(lr) => lr,
7109                None => continue,
7110            };
7111
7112            for node_id in node_ids {
7113                // Get the node data - store container ref to extend lifetime
7114                let node_data_container = layout_result.styled_dom.node_data.as_container();
7115                let node_data = match node_data_container.get(*node_id) {
7116                    Some(nd) => nd,
7117                    None => continue,
7118                };
7119
7120                // Check if this is an Image node with a callback
7121                let has_callback = matches!(node_data.get_node_type(), NodeType::Image(img_ref)
7122                    if img_ref.get_image_callback().is_some());
7123
7124                if !has_callback {
7125                    continue;
7126                }
7127
7128                // Get layout indices for this DOM node (can have multiple due to text splitting,
7129                // etc.)
7130                let layout_indices = match layout_result.layout_tree.dom_to_layout.get(node_id) {
7131                    Some(indices) if !indices.is_empty() => indices,
7132                    _ => continue,
7133                };
7134
7135                // Use the first layout index (primary node)
7136                let layout_index = layout_indices[0];
7137
7138                // Get the position from calculated_positions
7139                let position = match layout_result.calculated_positions.get(layout_index) {
7140                    Some(pos) => *pos,
7141                    None => continue,
7142                };
7143
7144                // Get the layout node to determine size
7145                let layout_node = match layout_result.layout_tree.get(layout_index) {
7146                    Some(ln) => ln,
7147                    None => continue,
7148                };
7149
7150                // Get the size from the layout node (used_size is the computed size from layout)
7151                let (width, height) = match layout_node.used_size {
7152                    Some(size) => (size.width, size.height),
7153                    None => continue, // Node hasn't been laid out yet
7154                };
7155
7156                let callback_domnode_id = DomNodeId {
7157                    dom: *dom_id,
7158                    node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(
7159                        *node_id,
7160                    )),
7161                };
7162
7163                let bounds = HidpiAdjustedBounds::from_bounds(
7164                    azul_css::props::basic::LayoutSize {
7165                        width: width as isize,
7166                        height: height as isize,
7167                    },
7168                    self.current_window_state.size.get_hidpi_factor(),
7169                );
7170
7171                // Create callback info
7172                let mut gl_callback_info = RenderImageCallbackInfo::new(
7173                    callback_domnode_id,
7174                    bounds,
7175                    gl_context,
7176                    &self.image_cache,
7177                    &self.font_manager.fc_cache,
7178                );
7179
7180                // Invoke the callback
7181                let new_image_ref = {
7182                    let mut node_data_mut = layout_result.styled_dom.node_data.as_container_mut();
7183                    match node_data_mut.get_mut(*node_id) {
7184                        Some(nd) => {
7185                            match &mut nd.node_type {
7186                                NodeType::Image(img_ref) => {
7187                                    img_ref.get_image_callback_mut().map(|core_callback| {
7188                                        // Convert from CoreImageCallback (cb: usize) to
7189                                        // RenderImageCallback (cb: fn pointer)
7190                                        let callback =
7191                                            RenderImageCallback::from_core(&core_callback.callback);
7192                                        (callback.cb)(
7193                                            core_callback.refany.clone(),
7194                                            gl_callback_info,
7195                                        )
7196                                    })
7197                                }
7198                                _ => None,
7199                            }
7200                        }
7201                        None => None,
7202                    }
7203                };
7204
7205                // Reset GL state after callback
7206                #[cfg(feature = "gl_context_loader")]
7207                if let Some(gl) = gl_context.as_ref() {
7208                    use gl_context_loader::gl;
7209                    gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
7210                    gl.disable(gl::FRAMEBUFFER_SRGB);
7211                    gl.disable(gl::MULTISAMPLE);
7212                }
7213
7214                // Extract the texture from the returned ImageRef
7215                if let Some(image_ref) = new_image_ref {
7216                    if let Some(decoded_image) = image_ref.into_inner() {
7217                        if let azul_core::resources::DecodedImage::Gl(texture) = decoded_image {
7218                            updated_textures.push((*dom_id, *node_id, texture));
7219                        }
7220                    }
7221                }
7222            }
7223        }
7224
7225        updated_textures
7226    }
7227
7228    /// Process IFrame updates requested by callbacks
7229    ///
7230    /// This method handles manual IFrame re-rendering triggered by `trigger_iframe_rerender()`.
7231    /// It invokes the IFrame callback with `DomRecreated` reason and performs layout on the
7232    /// returned DOM, then submits a new display list to WebRender for that pipeline.
7233    ///
7234    /// # Arguments
7235    ///
7236    /// * `iframes_to_update` - Map of DomId -> Set of NodeIds that need re-rendering
7237    /// * `window_state` - Current window state
7238    /// * `renderer_resources` - Renderer resources
7239    /// * `system_callbacks` - External system callbacks
7240    ///
7241    /// # Returns
7242    ///
7243    /// Vector of (DomId, NodeId) tuples for IFrames that were successfully updated
7244    pub fn process_iframe_updates(
7245        &mut self,
7246        iframes_to_update: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
7247        window_state: &FullWindowState,
7248        renderer_resources: &RendererResources,
7249        system_callbacks: &ExternalSystemCallbacks,
7250    ) -> Vec<(DomId, NodeId)> {
7251        let mut updated_iframes = Vec::new();
7252
7253        for (dom_id, node_ids) in iframes_to_update {
7254            for node_id in node_ids {
7255                // Extract iframe bounds from layout result
7256                let bounds = match Self::get_iframe_bounds_from_layout(
7257                    &self.layout_results,
7258                    *dom_id,
7259                    *node_id,
7260                ) {
7261                    Some(b) => b,
7262                    None => continue,
7263                };
7264
7265                // Force re-invocation by clearing the "was_invoked" flag
7266                self.iframe_manager.force_reinvoke(*dom_id, *node_id);
7267
7268                // Invoke the IFrame callback
7269                if let Some(_child_dom_id) = self.invoke_iframe_callback(
7270                    *dom_id,
7271                    *node_id,
7272                    bounds,
7273                    window_state,
7274                    renderer_resources,
7275                    system_callbacks,
7276                    &mut None,
7277                ) {
7278                    updated_iframes.push((*dom_id, *node_id));
7279                }
7280            }
7281        }
7282
7283        updated_iframes
7284    }
7285
7286    /// Queue IFrame updates to be processed in the next frame
7287    ///
7288    /// This is called after callbacks to store the iframes_to_update from CallbackChangeResult
7289    pub fn queue_iframe_updates(
7290        &mut self,
7291        iframes_to_update: BTreeMap<DomId, FastBTreeSet<NodeId>>,
7292    ) {
7293        for (dom_id, node_ids) in iframes_to_update {
7294            self.pending_iframe_updates
7295                .entry(dom_id)
7296                .or_insert_with(FastBTreeSet::new)
7297                .extend(node_ids);
7298        }
7299    }
7300
7301    /// Process and clear pending IFrame updates
7302    ///
7303    /// This is called during frame generation to re-render updated IFrames
7304    pub fn process_pending_iframe_updates(
7305        &mut self,
7306        window_state: &FullWindowState,
7307        renderer_resources: &RendererResources,
7308        system_callbacks: &ExternalSystemCallbacks,
7309    ) -> Vec<(DomId, NodeId)> {
7310        if self.pending_iframe_updates.is_empty() {
7311            return Vec::new();
7312        }
7313
7314        // Take ownership of pending updates
7315        let iframes_to_update = core::mem::take(&mut self.pending_iframe_updates);
7316
7317        // Process them
7318        self.process_iframe_updates(
7319            &iframes_to_update,
7320            window_state,
7321            renderer_resources,
7322            system_callbacks,
7323        )
7324    }
7325
7326    /// Helper: Extract IFrame bounds from layout results
7327    ///
7328    /// Returns None if the node is not an IFrame or doesn't have layout info
7329    fn get_iframe_bounds_from_layout(
7330        layout_results: &BTreeMap<DomId, DomLayoutResult>,
7331        dom_id: DomId,
7332        node_id: NodeId,
7333    ) -> Option<LogicalRect> {
7334        let layout_result = layout_results.get(&dom_id)?;
7335
7336        // Check if this is an IFrame node
7337        let node_data_container = layout_result.styled_dom.node_data.as_container();
7338        let node_data = node_data_container.get(node_id)?;
7339
7340        if !matches!(node_data.get_node_type(), NodeType::IFrame(_)) {
7341            return None;
7342        }
7343
7344        // Get layout indices
7345        let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
7346        if layout_indices.is_empty() {
7347            return None;
7348        }
7349
7350        let layout_index = layout_indices[0];
7351
7352        // Get position
7353        let position = *layout_result.calculated_positions.get(layout_index)?;
7354
7355        // Get size
7356        let layout_node = layout_result.layout_tree.get(layout_index)?;
7357        let size = layout_node.used_size?;
7358
7359        Some(LogicalRect::new(
7360            position,
7361            LogicalSize::new(size.width as f32, size.height as f32),
7362        ))
7363    }
7364}
7365
7366#[cfg(feature = "a11y")]
7367#[derive(Debug, Clone)]
7368pub enum TextEditType {
7369    ReplaceSelection(String),
7370    SetValue(String),
7371    SetNumericValue(f64),
7372}