Skip to main content

astrelis_ui/
tree.rs

1//! UI tree structure with Taffy layout integration.
2
3use crate::constraint_resolver::{ConstraintResolver, ResolveContext};
4use crate::dirty::{DirtyCounters, DirtyFlags, StyleGuard};
5use crate::metrics::{DirtyStats, MetricsTimer, UiMetrics};
6use crate::plugin::registry::WidgetTypeRegistry;
7use crate::style::Style;
8use crate::widgets::Widget;
9#[cfg(feature = "docking")]
10use crate::widgets::docking::{DockSplitter, DockTabs};
11use astrelis_core::alloc::HashSet;
12use astrelis_core::math::Vec2;
13use astrelis_core::profiling::{profile_function, profile_scope};
14use astrelis_text::FontRenderer;
15use astrelis_text::ShapedTextData;
16use indexmap::IndexMap;
17use std::sync::Arc;
18use taffy::{TaffyTree, prelude::*};
19
20/// Node identifier in the UI tree.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct NodeId(pub usize);
23
24/// Layout information computed by Taffy.
25#[derive(Debug, Clone, Copy)]
26pub struct LayoutRect {
27    pub x: f32,
28    pub y: f32,
29    pub width: f32,
30    pub height: f32,
31}
32
33impl LayoutRect {
34    pub fn contains(&self, point: Vec2) -> bool {
35        point.x >= self.x
36            && point.x <= self.x + self.width
37            && point.y >= self.y
38            && point.y <= self.y + self.height
39    }
40
41    pub fn position(&self) -> Vec2 {
42        Vec2::new(self.x, self.y)
43    }
44
45    pub fn size(&self) -> Vec2 {
46        Vec2::new(self.width, self.height)
47    }
48}
49
50#[cfg(feature = "docking")]
51/// Internal enum for collecting docking layout info before processing.
52enum DockingLayoutInfo {
53    Splitter {
54        children: Vec<NodeId>,
55        direction: crate::widgets::docking::SplitDirection,
56        split_ratio: f32,
57        separator_size: f32,
58        parent_layout: LayoutRect,
59    },
60    Tabs {
61        children: Vec<NodeId>,
62        active_tab: usize,
63        tab_bar_height: f32,
64        content_padding: f32,
65        parent_layout: LayoutRect,
66    },
67}
68
69/// A node in the UI tree.
70pub struct UiNode {
71    pub widget: Box<dyn Widget>,
72    pub taffy_node: taffy::NodeId,
73    pub layout: LayoutRect,
74    pub dirty_flags: DirtyFlags,
75    pub parent: Option<NodeId>,
76    pub children: Vec<NodeId>,
77    /// Cached text measurement (width, height)
78    pub text_measurement: Option<(f32, f32)>,
79    /// Version counters for cache invalidation
80    pub layout_version: u32,
81    pub text_version: u32,
82    pub paint_version: u32,
83    /// Cached shaped text data (Phase 3)
84    pub text_cache: Option<Arc<ShapedTextData>>,
85    /// Accumulated z_index from all parent containers.
86    /// Computed during layout traversal.
87    pub computed_z_index: u16,
88    /// Accumulated opacity from all parent containers.
89    /// Computed during layout traversal: `parent_opacity * style.opacity`.
90    pub computed_opacity: f32,
91    /// Accumulated visual translation from all parent containers.
92    /// Computed during layout traversal: `parent_translate + style.translate`.
93    pub computed_translate: Vec2,
94    /// Accumulated visual scale from all parent containers.
95    /// Computed during layout traversal: `parent_scale * style.scale` (per-axis).
96    pub computed_scale: Vec2,
97}
98
99impl UiNode {
100    /// Bump version counters based on dirty flags and invalidate caches.
101    ///
102    /// This is called automatically when dirty flags are set to ensure
103    /// cached data is invalidated when it becomes stale.
104    pub fn bump_version(&mut self, flags: DirtyFlags) {
105        if flags.intersects(DirtyFlags::LAYOUT | DirtyFlags::CHILDREN_ORDER) {
106            self.layout_version = self.layout_version.wrapping_add(1);
107        }
108        if flags.contains(DirtyFlags::TEXT_SHAPING) {
109            self.text_version = self.text_version.wrapping_add(1);
110            // Invalidate text cache when text changes
111            self.text_cache = None;
112        }
113        if flags.intersects(DirtyFlags::COLOR | DirtyFlags::OPACITY | DirtyFlags::TRANSFORM) {
114            self.paint_version = self.paint_version.wrapping_add(1);
115        }
116    }
117}
118
119/// UI tree managing widgets and layout.
120pub struct UiTree {
121    taffy: TaffyTree<()>,
122    nodes: IndexMap<NodeId, UiNode>,
123    root: Option<NodeId>,
124    next_id: usize,
125    /// Set of dirty nodes that need layout recomputation
126    dirty_nodes: HashSet<NodeId>,
127    /// Roots of dirty subtrees (for selective layout)
128    dirty_roots: HashSet<NodeId>,
129    /// Performance metrics from last update
130    last_metrics: Option<UiMetrics>,
131    /// Nodes with viewport-dependent constraints (vw, vh, vmin, vmax, calc, min, max, clamp)
132    viewport_constraint_nodes: HashSet<NodeId>,
133    /// Nodes removed since the last drain (for renderer cleanup).
134    removed_nodes: Vec<NodeId>,
135    /// O(1) dirty state counters.
136    dirty_counters: DirtyCounters,
137    /// Global content padding for docking tab panels (set from DockingStyle before layout).
138    #[cfg(feature = "docking")]
139    docking_content_padding: f32,
140}
141
142impl UiTree {
143    /// Create a new UI tree.
144    pub fn new() -> Self {
145        Self {
146            taffy: TaffyTree::new(),
147            nodes: IndexMap::new(),
148            root: None,
149            next_id: 0,
150            dirty_nodes: HashSet::new(),
151            dirty_roots: HashSet::new(),
152            last_metrics: None,
153            viewport_constraint_nodes: HashSet::new(),
154            removed_nodes: Vec::new(),
155            dirty_counters: DirtyCounters::new(),
156            #[cfg(feature = "docking")]
157            docking_content_padding: 4.0,
158        }
159    }
160
161    /// Set the global docking content padding (called before layout from DockingStyle).
162    #[cfg(feature = "docking")]
163    pub fn set_docking_content_padding(&mut self, padding: f32) {
164        self.docking_content_padding = padding;
165    }
166
167    /// Add a widget to the tree and return its NodeId.
168    pub fn add_widget(&mut self, widget: Box<dyn Widget>) -> NodeId {
169        let node_id = NodeId(self.next_id);
170        self.next_id += 1;
171
172        // Track nodes with viewport-dependent constraints
173        if widget.style().has_unresolved_constraints() {
174            self.viewport_constraint_nodes.insert(node_id);
175        }
176
177        // Create Taffy node with widget's style
178        let style = widget.style().layout.clone();
179        let taffy_node = self
180            .taffy
181            .new_leaf(style)
182            .expect("Failed to create taffy node");
183
184        let ui_node = UiNode {
185            widget,
186            taffy_node,
187            layout: LayoutRect {
188                x: 0.0,
189                y: 0.0,
190                width: 0.0,
191                height: 0.0,
192            },
193            dirty_flags: DirtyFlags::NONE,
194            parent: None,
195            children: Vec::new(),
196            text_measurement: None,
197            layout_version: 0,
198            text_version: 0,
199            paint_version: 0,
200            text_cache: None,
201            computed_z_index: 0,
202            computed_opacity: 1.0,
203            computed_translate: Vec2::ZERO,
204            computed_scale: Vec2::ONE,
205        };
206
207        self.nodes.insert(node_id, ui_node);
208        self.mark_dirty_flags(node_id, DirtyFlags::LAYOUT);
209
210        node_id
211    }
212
213    /// Set a node as a child of another node.
214    pub fn add_child(&mut self, parent: NodeId, child: NodeId) {
215        if let (Some(parent_node), Some(child_node)) =
216            (self.nodes.get(&parent), self.nodes.get(&child))
217        {
218            self.taffy
219                .add_child(parent_node.taffy_node, child_node.taffy_node)
220                .ok();
221
222            // Update parent/child relationships
223            if let Some(child_node) = self.nodes.get_mut(&child) {
224                child_node.parent = Some(parent);
225            }
226            if let Some(parent_node) = self.nodes.get_mut(&parent) {
227                parent_node.children.push(child);
228            }
229
230            self.mark_dirty_flags(parent, DirtyFlags::CHILDREN_ORDER);
231        }
232    }
233
234    /// Set multiple children for a node.
235    pub fn set_children(&mut self, parent: NodeId, children: &[NodeId]) {
236        if let Some(parent_node) = self.nodes.get(&parent) {
237            let taffy_children: Vec<taffy::NodeId> = children
238                .iter()
239                .filter_map(|id| self.nodes.get(id).map(|n| n.taffy_node))
240                .collect();
241
242            self.taffy
243                .set_children(parent_node.taffy_node, &taffy_children)
244                .ok();
245
246            // Update parent/child relationships
247            for &child_id in children {
248                if let Some(child_node) = self.nodes.get_mut(&child_id) {
249                    child_node.parent = Some(parent);
250                }
251            }
252            if let Some(parent_node) = self.nodes.get_mut(&parent) {
253                parent_node.children = children.to_vec();
254            }
255
256            self.mark_dirty_flags(parent, DirtyFlags::CHILDREN_ORDER);
257        }
258    }
259
260    /// Set the root node.
261    pub fn set_root(&mut self, node_id: NodeId) {
262        self.root = Some(node_id);
263        self.mark_dirty_flags(node_id, DirtyFlags::LAYOUT);
264    }
265
266    /// Check if a node is a layout boundary (fixed size).
267    fn is_layout_boundary(node: &UiNode) -> bool {
268        let style = &node.widget.style().layout;
269        matches!(style.size.width, Dimension::Length(_))
270            && matches!(style.size.height, Dimension::Length(_))
271    }
272
273    /// Mark a node with specific dirty flags and propagate to ancestors if needed.
274    pub fn mark_dirty_flags(&mut self, node_id: NodeId, flags: DirtyFlags) {
275        profile_function!();
276
277        if flags.is_empty() {
278            return;
279        }
280
281        let needs_propagation = self.mark_node_dirty_inner(node_id, flags);
282
283        // Propagate to ancestors if needed
284        if needs_propagation {
285            self.propagate_dirty_to_ancestors(node_id, flags);
286        }
287    }
288
289    /// Inner dirty marking: sets flags, bumps versions, notifies Taffy.
290    /// Returns `true` if ancestor propagation is needed.
291    fn mark_node_dirty_inner(&mut self, node_id: NodeId, flags: DirtyFlags) -> bool {
292        self.dirty_nodes.insert(node_id);
293
294        let Some(node) = self.nodes.get_mut(&node_id) else {
295            return false;
296        };
297
298        let old_flags = node.dirty_flags;
299        node.dirty_flags |= flags;
300        self.dirty_counters.on_mark(old_flags, flags);
301
302        // Notify Taffy of changes
303        if flags
304            .intersects(DirtyFlags::LAYOUT | DirtyFlags::CHILDREN_ORDER | DirtyFlags::TEXT_SHAPING)
305        {
306            self.taffy.mark_dirty(node.taffy_node).ok();
307        }
308
309        // Bump version counters
310        if flags.intersects(DirtyFlags::LAYOUT | DirtyFlags::CHILDREN_ORDER) {
311            node.layout_version = node.layout_version.wrapping_add(1);
312            // Layout changes can affect text wrapping width, invalidate measurement
313            node.text_measurement = None;
314        }
315        if flags.contains(DirtyFlags::TEXT_SHAPING) {
316            node.text_version = node.text_version.wrapping_add(1);
317            node.text_measurement = None; // Invalidate measurement cache
318            node.text_cache = None; // Invalidate shaped text cache
319        }
320        if flags.intersects(
321            DirtyFlags::COLOR
322                | DirtyFlags::OPACITY
323                | DirtyFlags::GEOMETRY
324                | DirtyFlags::IMAGE
325                | DirtyFlags::FOCUS
326                | DirtyFlags::SCROLL
327                | DirtyFlags::Z_INDEX
328                | DirtyFlags::TRANSFORM,
329        ) {
330            node.paint_version = node.paint_version.wrapping_add(1);
331        }
332        if flags.contains(DirtyFlags::VISIBILITY) {
333            node.layout_version = node.layout_version.wrapping_add(1);
334        }
335
336        flags.should_propagate_to_parent()
337    }
338
339    /// Propagate dirty flags from a node up to its ancestors.
340    fn propagate_dirty_to_ancestors(&mut self, node_id: NodeId, flags: DirtyFlags) {
341        let propagation_flags = flags.propagation_flags();
342
343        let Some(node) = self.nodes.get(&node_id) else {
344            return;
345        };
346
347        // Check if this node is a layout boundary
348        if Self::is_layout_boundary(node) {
349            self.dirty_roots.insert(node_id);
350            return;
351        }
352
353        let mut current_parent = node.parent;
354
355        while let Some(parent_id) = current_parent {
356            if !self.dirty_nodes.insert(parent_id) {
357                // Already marked, check if we need to add more flags
358                if let Some(parent_node) = self.nodes.get(&parent_id)
359                    && parent_node.dirty_flags.contains(propagation_flags)
360                {
361                    // Already has these flags, stop propagation
362                    break;
363                }
364            }
365
366            if let Some(parent_node) = self.nodes.get_mut(&parent_id) {
367                parent_node.dirty_flags |= propagation_flags;
368                if propagation_flags.contains(DirtyFlags::LAYOUT) {
369                    parent_node.layout_version = parent_node.layout_version.wrapping_add(1);
370                }
371
372                if Self::is_layout_boundary(parent_node) {
373                    self.dirty_roots.insert(parent_id);
374                    return;
375                }
376
377                current_parent = parent_node.parent;
378            } else {
379                break;
380            }
381        }
382
383        // If we reached here, we hit the top without a boundary.
384        if let Some(root) = self.root
385            && self.dirty_nodes.contains(&root)
386        {
387            self.dirty_roots.insert(root);
388        }
389    }
390
391    /// Mark multiple nodes dirty in a batch with deduplicated ancestor propagation.
392    ///
393    /// This is more efficient than calling `mark_dirty_flags` in a loop when
394    /// multiple sibling nodes need marking, since ancestor walks are deduplicated.
395    pub fn mark_dirty_batch(&mut self, updates: &[(NodeId, DirtyFlags)]) {
396        profile_function!();
397
398        if updates.is_empty() {
399            return;
400        }
401
402        // Phase 1: Mark all flags, counters, versions (no propagation)
403        // Collect nodes that need ancestor propagation
404        let mut needs_propagation: Vec<(NodeId, DirtyFlags)> = Vec::new();
405        for &(node_id, flags) in updates {
406            if flags.is_empty() {
407                continue;
408            }
409            let needs_prop = self.mark_node_dirty_inner(node_id, flags);
410            if needs_prop {
411                needs_propagation.push((node_id, flags));
412            }
413        }
414
415        // Phase 2: Deduplicated ancestor propagation
416        // Nodes already in dirty_nodes with the right propagation flags will short-circuit
417        // the walk, so siblings sharing ancestors naturally deduplicate.
418        for (node_id, flags) in needs_propagation {
419            self.propagate_dirty_to_ancestors(node_id, flags);
420        }
421    }
422
423    /// Mark all nodes with the same dirty flags efficiently.
424    ///
425    /// This is a fast path for operations like theme changes where every node
426    /// gets the same flags. For non-propagating flags (COLOR, OPACITY, etc.),
427    /// this skips all ancestor logic and does a simple O(N) pass.
428    pub fn mark_all_dirty_uniform(&mut self, flags: DirtyFlags) {
429        profile_function!();
430
431        if flags.is_empty() {
432            return;
433        }
434
435        let needs_propagation = flags.should_propagate_to_parent();
436
437        // Fast path: iterate all nodes directly, mark flags and bump versions
438        for (&node_id, node) in self.nodes.iter_mut() {
439            let old_flags = node.dirty_flags;
440            node.dirty_flags |= flags;
441            self.dirty_counters.on_mark(old_flags, flags);
442            self.dirty_nodes.insert(node_id);
443
444            // Notify Taffy if needed
445            if flags.intersects(
446                DirtyFlags::LAYOUT | DirtyFlags::CHILDREN_ORDER | DirtyFlags::TEXT_SHAPING,
447            ) {
448                self.taffy.mark_dirty(node.taffy_node).ok();
449            }
450
451            // Bump version counters
452            if flags.intersects(DirtyFlags::LAYOUT | DirtyFlags::CHILDREN_ORDER) {
453                node.layout_version = node.layout_version.wrapping_add(1);
454                node.text_measurement = None;
455            }
456            if flags.contains(DirtyFlags::TEXT_SHAPING) {
457                node.text_version = node.text_version.wrapping_add(1);
458                node.text_measurement = None;
459                node.text_cache = None;
460            }
461            if flags.intersects(
462                DirtyFlags::COLOR
463                    | DirtyFlags::OPACITY
464                    | DirtyFlags::GEOMETRY
465                    | DirtyFlags::IMAGE
466                    | DirtyFlags::FOCUS
467                    | DirtyFlags::SCROLL
468                    | DirtyFlags::Z_INDEX,
469            ) {
470                node.paint_version = node.paint_version.wrapping_add(1);
471            }
472            if flags.contains(DirtyFlags::VISIBILITY) {
473                node.layout_version = node.layout_version.wrapping_add(1);
474            }
475        }
476
477        // For propagating flags, mark the root as dirty root since all nodes are dirty
478        if needs_propagation && let Some(root) = self.root {
479            self.dirty_roots.insert(root);
480        }
481    }
482
483    /// Clear all dirty flags after rendering (called by renderer).
484    ///
485    /// Only iterates the dirty nodes set rather than all nodes for O(dirty) complexity.
486    pub fn clear_dirty_flags(&mut self) {
487        for &node_id in &self.dirty_nodes {
488            if let Some(node) = self.nodes.get_mut(&node_id) {
489                self.dirty_counters.on_clear(node.dirty_flags);
490                node.dirty_flags = DirtyFlags::NONE;
491            }
492        }
493        self.dirty_nodes.clear();
494        self.dirty_roots.clear();
495    }
496
497    /// Get the root node.
498    pub fn root(&self) -> Option<NodeId> {
499        self.root
500    }
501
502    /// Get a widget by node ID.
503    pub fn get_widget(&self, node_id: NodeId) -> Option<&dyn Widget> {
504        self.nodes.get(&node_id).map(|n| &*n.widget)
505    }
506
507    /// Get a mutable widget by node ID.
508    pub fn get_widget_mut(&mut self, node_id: NodeId) -> Option<&mut dyn Widget> {
509        self.nodes.get_mut(&node_id).map(|n| &mut *n.widget)
510    }
511
512    /// Get layout for a node.
513    pub fn get_layout(&self, node_id: NodeId) -> Option<LayoutRect> {
514        self.nodes.get(&node_id).map(|n| n.layout)
515    }
516
517    /// Check if tree needs layout.
518    pub fn is_dirty(&self) -> bool {
519        !self.dirty_nodes.is_empty()
520    }
521
522    /// O(1) check: any node needs layout recomputation?
523    pub fn has_layout_dirty(&self) -> bool {
524        self.dirty_counters.has_layout_dirty()
525    }
526
527    /// O(1) check: any node needs text shaping?
528    pub fn has_text_dirty(&self) -> bool {
529        self.dirty_counters.has_text_dirty()
530    }
531
532    /// Get a snapshot of current dirty counter state (for metrics).
533    pub fn dirty_summary(&self) -> crate::dirty::DirtySummary {
534        self.dirty_counters.summary()
535    }
536
537    /// Get the dirty roots for selective tree traversal.
538    ///
539    /// Dirty roots are the topmost nodes in dirty subtrees. Starting traversal
540    /// from these nodes allows skipping clean subtrees entirely.
541    pub fn dirty_roots(&self) -> &HashSet<NodeId> {
542        &self.dirty_roots
543    }
544
545    /// Get the set of all dirty nodes.
546    pub fn dirty_nodes(&self) -> &HashSet<NodeId> {
547        &self.dirty_nodes
548    }
549
550    /// Get the last computed metrics.
551    pub fn last_metrics(&self) -> Option<&UiMetrics> {
552        self.last_metrics.as_ref()
553    }
554
555    /// Get immutable reference to a node.
556    pub(crate) fn get_node(&self, node_id: NodeId) -> Option<&UiNode> {
557        self.nodes.get(&node_id)
558    }
559
560    /// Get mutable reference to a node.
561    pub(crate) fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut UiNode> {
562        self.nodes.get_mut(&node_id)
563    }
564
565    /// Register a widget ID to node ID mapping (for builder API).
566    pub fn register_widget(&mut self, widget_id: crate::widget_id::WidgetId, node_id: NodeId) {
567        // Store mapping in widget registry if tree has one
568        // For now, this is a no-op as the mapping is managed by UiCore/UiSystem
569        // This method exists for builder API compatibility
570        let _ = (widget_id, node_id);
571    }
572
573    /// Create a style guard for automatic dirty marking on style changes.
574    ///
575    /// The guard automatically marks appropriate dirty flags when dropped
576    /// if the style's layout-affecting properties changed.
577    ///
578    /// # Example
579    /// ```ignore
580    /// let mut guard = tree.style_guard_mut(node_id);
581    /// if let Some(style) = guard.layout_mut() {
582    ///     style.padding = Rect::all(length(10.0));
583    /// }
584    /// // Automatically marks LAYOUT flag on drop if padding changed
585    /// ```
586    pub fn style_guard_mut(&mut self, node_id: NodeId) -> StyleGuard<'_> {
587        StyleGuard::new(self, node_id)
588    }
589
590    /// Update text content with automatic dirty marking.
591    ///
592    /// Marks TEXT_SHAPING flag if content changed.
593    pub fn update_text_content(&mut self, node_id: NodeId, new_content: impl Into<String>) -> bool {
594        if let Some(node) = self.nodes.get_mut(&node_id) {
595            // Try to downcast to Text widget
596            if let Some(text) = node
597                .widget
598                .as_any_mut()
599                .downcast_mut::<crate::widgets::Text>()
600            {
601                let changed = text.set_content(new_content);
602                if changed {
603                    self.mark_dirty_flags(node_id, DirtyFlags::TEXT_SHAPING);
604                }
605                return changed;
606            }
607        }
608        false
609    }
610
611    /// Update color with automatic dirty marking.
612    ///
613    /// Marks COLOR flag (doesn't require layout recomputation).
614    pub fn update_color(&mut self, node_id: NodeId, new_color: astrelis_render::Color) -> bool {
615        if let Some(node) = self.nodes.get_mut(&node_id) {
616            let old_color = node.widget.style().background_color;
617            node.widget.style_mut().background_color = Some(new_color);
618
619            if old_color != Some(new_color) {
620                self.mark_dirty_flags(node_id, DirtyFlags::COLOR);
621                return true;
622            }
623        }
624        false
625    }
626
627    /// Update opacity with automatic dirty marking.
628    ///
629    /// Marks OPACITY flag and propagates down to descendants.
630    /// Returns true if the value changed.
631    pub fn update_opacity(&mut self, node_id: NodeId, opacity: f32) -> bool {
632        let opacity = opacity.clamp(0.0, 1.0);
633        if let Some(node) = self.nodes.get_mut(&node_id) {
634            let old = node.widget.style().opacity;
635            if (old - opacity).abs() < f32::EPSILON {
636                return false;
637            }
638            node.widget.style_mut().set_opacity(opacity);
639            self.mark_opacity_dirty(node_id);
640            true
641        } else {
642            false
643        }
644    }
645
646    /// Update visual translation with automatic dirty marking.
647    ///
648    /// Returns true if the value changed.
649    pub fn update_translate(&mut self, node_id: NodeId, translate: Vec2) -> bool {
650        if let Some(node) = self.nodes.get_mut(&node_id) {
651            let old = node.widget.style().translate;
652            if old == translate {
653                return false;
654            }
655            node.widget.style_mut().set_translate(translate);
656            self.mark_transform_dirty(node_id);
657            true
658        } else {
659            false
660        }
661    }
662
663    /// Update visual X translation with automatic dirty marking.
664    pub fn update_translate_x(&mut self, node_id: NodeId, x: f32) -> bool {
665        if let Some(node) = self.nodes.get_mut(&node_id) {
666            let old = node.widget.style().translate.x;
667            if (old - x).abs() < f32::EPSILON {
668                return false;
669            }
670            node.widget.style_mut().set_translate_x(x);
671            self.mark_transform_dirty(node_id);
672            true
673        } else {
674            false
675        }
676    }
677
678    /// Update visual Y translation with automatic dirty marking.
679    pub fn update_translate_y(&mut self, node_id: NodeId, y: f32) -> bool {
680        if let Some(node) = self.nodes.get_mut(&node_id) {
681            let old = node.widget.style().translate.y;
682            if (old - y).abs() < f32::EPSILON {
683                return false;
684            }
685            node.widget.style_mut().set_translate_y(y);
686            self.mark_transform_dirty(node_id);
687            true
688        } else {
689            false
690        }
691    }
692
693    /// Update visual scale with automatic dirty marking.
694    pub fn update_scale(&mut self, node_id: NodeId, scale: Vec2) -> bool {
695        if let Some(node) = self.nodes.get_mut(&node_id) {
696            let old = node.widget.style().scale;
697            if old == scale {
698                return false;
699            }
700            node.widget.style_mut().set_scale(scale);
701            self.mark_transform_dirty(node_id);
702            true
703        } else {
704            false
705        }
706    }
707
708    /// Update visual X scale with automatic dirty marking.
709    pub fn update_scale_x(&mut self, node_id: NodeId, x: f32) -> bool {
710        if let Some(node) = self.nodes.get_mut(&node_id) {
711            let old = node.widget.style().scale.x;
712            if (old - x).abs() < f32::EPSILON {
713                return false;
714            }
715            node.widget.style_mut().scale.x = x;
716            self.mark_transform_dirty(node_id);
717            true
718        } else {
719            false
720        }
721    }
722
723    /// Update visual Y scale with automatic dirty marking.
724    pub fn update_scale_y(&mut self, node_id: NodeId, y: f32) -> bool {
725        if let Some(node) = self.nodes.get_mut(&node_id) {
726            let old = node.widget.style().scale.y;
727            if (old - y).abs() < f32::EPSILON {
728                return false;
729            }
730            node.widget.style_mut().scale.y = y;
731            self.mark_transform_dirty(node_id);
732            true
733        } else {
734            false
735        }
736    }
737
738    /// Set visibility with automatic dirty marking and Taffy display switching.
739    ///
740    /// When `visible = false`, sets Taffy `display = None` (collapse from layout).
741    /// When `visible = true`, restores the previous display mode.
742    pub fn set_visible(&mut self, node_id: NodeId, visible: bool) -> bool {
743        if let Some(node) = self.nodes.get_mut(&node_id) {
744            let old = node.widget.style().visible;
745            if old == visible {
746                return false;
747            }
748            node.widget.style_mut().set_visible(visible);
749
750            // Switch Taffy display mode
751            if visible {
752                // Restore to Flex (the default layout mode)
753                node.widget.style_mut().layout.display = taffy::Display::Flex;
754            } else {
755                node.widget.style_mut().layout.display = taffy::Display::None;
756            }
757
758            // Sync to Taffy layout node
759            let taffy_node = node.taffy_node;
760            let taffy_style = node.widget.style().layout.clone();
761            self.taffy.set_style(taffy_node, taffy_style).ok();
762
763            self.mark_dirty_flags(node_id, DirtyFlags::LAYOUT | DirtyFlags::VISIBILITY);
764            true
765        } else {
766            false
767        }
768    }
769
770    /// Toggle visibility with automatic dirty marking.
771    pub fn toggle_visible(&mut self, node_id: NodeId) -> bool {
772        if let Some(node) = self.nodes.get(&node_id) {
773            let current = node.widget.style().visible;
774            self.set_visible(node_id, !current)
775        } else {
776            false
777        }
778    }
779
780    /// Compute layout for all nodes.
781    /// Compute layout with performance metrics collection.
782    pub fn compute_layout_instrumented(
783        &mut self,
784        viewport_size: astrelis_core::geometry::Size<f32>,
785        font_renderer: Option<&FontRenderer>,
786        widget_registry: &WidgetTypeRegistry,
787    ) -> UiMetrics {
788        profile_function!();
789
790        let total_timer = MetricsTimer::start();
791        let mut metrics = UiMetrics::new();
792        metrics.total_nodes = self.nodes.len();
793
794        // Collect dirty stats
795        let mut dirty_stats = DirtyStats::new();
796        for node in self.nodes.values() {
797            dirty_stats.add_node(node.dirty_flags);
798        }
799        metrics.nodes_layout_dirty = dirty_stats.layout_count;
800        metrics.nodes_text_dirty = dirty_stats.text_count;
801        metrics.nodes_paint_dirty = dirty_stats.paint_count;
802        metrics.nodes_geometry_dirty = dirty_stats.geometry_count;
803
804        // Early exit if nothing to do
805        if self.dirty_nodes.is_empty() {
806            metrics.total_time = total_timer.stop();
807            self.last_metrics = Some(metrics.clone());
808            return metrics;
809        }
810
811        // Skip layout if no layout-affecting changes
812        if !self.has_layout_dirty() {
813            metrics.layout_skips = self.nodes.len();
814            metrics.total_time = total_timer.stop();
815            self.last_metrics = Some(metrics.clone());
816            return metrics;
817        }
818
819        let layout_timer = MetricsTimer::start();
820        self.compute_layout_internal(viewport_size, font_renderer, widget_registry);
821        metrics.layout_time = layout_timer.stop();
822
823        metrics.total_time = total_timer.stop();
824        self.last_metrics = Some(metrics.clone());
825        metrics
826    }
827
828    /// Compute layout (standard API without metrics).
829    pub fn compute_layout(
830        &mut self,
831        size: astrelis_core::geometry::Size<f32>,
832        font_renderer: Option<&FontRenderer>,
833        widget_registry: &WidgetTypeRegistry,
834    ) {
835        profile_function!();
836
837        // Skip if nothing to do
838        if self.dirty_nodes.is_empty() {
839            return;
840        }
841
842        // Skip layout if no layout-affecting changes
843        // Don't clear dirty flags - renderer needs them for visual updates
844        if !self.has_layout_dirty() {
845            return;
846        }
847
848        self.compute_layout_internal(size, font_renderer, widget_registry);
849        // Don't clear flags here - renderer will clear them after processing
850    }
851
852    /// Internal layout computation implementation.
853    ///
854    /// Always computes layout from the tree root to ensure correct absolute positioning.
855    /// The subtree optimization was removed because it caused positioning bugs when
856    /// layout boundaries (fixed-size nodes) stopped dirty propagation but Taffy computed
857    /// positions relative to subtree roots instead of the tree root.
858    fn compute_layout_internal(
859        &mut self,
860        viewport_size: astrelis_core::geometry::Size<f32>,
861        font_renderer: Option<&FontRenderer>,
862        widget_registry: &WidgetTypeRegistry,
863    ) {
864        profile_scope!("compute_layout_internal");
865
866        // Resolve viewport-relative units before layout computation
867        self.resolve_viewport_units(viewport_size);
868
869        // Always compute layout from tree root for correct positioning
870        let Some(root_id) = self.root else { return };
871        let Some(root_node) = self.nodes.get(&root_id) else {
872            return;
873        };
874        let root_taffy_node = root_node.taffy_node;
875
876        let available_space = Size {
877            width: AvailableSpace::Definite(viewport_size.width),
878            height: AvailableSpace::Definite(viewport_size.height),
879        };
880
881        let nodes_ptr = &mut self.nodes as *mut IndexMap<NodeId, UiNode>;
882
883        let measure_func = |known_dimensions: Size<Option<f32>>,
884                            available_space: Size<AvailableSpace>,
885                            node_id: taffy::NodeId,
886                            _node_context: Option<&mut ()>,
887                            _style: &taffy::Style|
888         -> Size<f32> {
889            // SAFETY: nodes_ptr is valid during layout computation
890            let nodes = unsafe { &mut *nodes_ptr };
891
892            let (widget, cached_measurement) = nodes
893                .values_mut()
894                .find(|node| node.taffy_node == node_id)
895                .map(|node| (&node.widget, &mut node.text_measurement))
896                .unzip();
897
898            if let (Some(widget), Some(cached_measurement)) = (widget, cached_measurement) {
899                if let Some((cached_w, cached_h)) = *cached_measurement {
900                    return Size {
901                        width: known_dimensions.width.unwrap_or(cached_w),
902                        height: known_dimensions.height.unwrap_or(cached_h),
903                    };
904                }
905
906                let available = Vec2::new(
907                    match available_space.width {
908                        AvailableSpace::Definite(w) => w,
909                        AvailableSpace::MinContent => 0.0,
910                        AvailableSpace::MaxContent => f32::MAX,
911                    },
912                    match available_space.height {
913                        AvailableSpace::Definite(h) => h,
914                        AvailableSpace::MinContent => 0.0,
915                        AvailableSpace::MaxContent => f32::MAX,
916                    },
917                );
918
919                let measured = widget.measure(available, font_renderer);
920
921                // Cache measurement for widget types that opt in (e.g. Text)
922                if widget_registry.caches_measurement(widget.as_any().type_id()) {
923                    *cached_measurement = Some((measured.x, measured.y));
924                }
925
926                Size {
927                    width: known_dimensions.width.unwrap_or(measured.x),
928                    height: known_dimensions.height.unwrap_or(measured.y),
929                }
930            } else {
931                Size {
932                    width: known_dimensions.width.unwrap_or(0.0),
933                    height: known_dimensions.height.unwrap_or(0.0),
934                }
935            }
936        };
937
938        self.taffy
939            .compute_layout_with_measure(root_taffy_node, available_space, measure_func)
940            .ok();
941
942        // Update ALL nodes from tree root
943        self.update_subtree_layout(root_id);
944
945        // Post-process docking widgets to override child layouts
946        #[cfg(feature = "docking")]
947        self.post_process_docking_layouts(root_id);
948    }
949
950    /// Post-process layouts for DockSplitter and DockTabs widgets.
951    ///
952    /// These widgets have custom layout logic that can't be expressed in Taffy:
953    /// - DockSplitter: positions children based on split ratio
954    /// - DockTabs: children fill the content area below the tab bar
955    ///
956    /// This function recursively processes the tree from root to ensure
957    /// parent layouts are computed before children (important for nested docking).
958    #[cfg(feature = "docking")]
959    fn post_process_docking_layouts(&mut self, node_id: NodeId) {
960        profile_scope!("post_process_docking_layouts");
961
962        // Get info for this node first
963        let info = {
964            let Some(node) = self.nodes.get(&node_id) else {
965                return;
966            };
967
968            node.widget
969                .as_any()
970                .downcast_ref::<DockSplitter>()
971                .map(|splitter| DockingLayoutInfo::Splitter {
972                    children: splitter.children.clone(),
973                    direction: splitter.direction,
974                    split_ratio: splitter.split_ratio,
975                    separator_size: splitter.separator_size,
976                    parent_layout: node.layout,
977                })
978                .or_else(|| {
979                    node.widget.as_any().downcast_ref::<DockTabs>().map(|tabs| {
980                        let content_padding =
981                            tabs.content_padding.unwrap_or(self.docking_content_padding);
982                        DockingLayoutInfo::Tabs {
983                            children: tabs.children.clone(),
984                            active_tab: tabs.active_tab,
985                            tab_bar_height: tabs.theme.tab_bar_height,
986                            content_padding,
987                            parent_layout: node.layout,
988                        }
989                    })
990                })
991        };
992
993        // Apply layout if this is a docking widget
994        let children_to_recurse = match info {
995            Some(DockingLayoutInfo::Splitter {
996                children,
997                direction,
998                split_ratio,
999                separator_size,
1000                parent_layout,
1001            }) => {
1002                self.apply_splitter_layout(
1003                    children.clone(),
1004                    direction,
1005                    split_ratio,
1006                    separator_size,
1007                    parent_layout,
1008                );
1009                children
1010            }
1011            Some(DockingLayoutInfo::Tabs {
1012                children,
1013                active_tab,
1014                tab_bar_height,
1015                content_padding,
1016                parent_layout,
1017            }) => {
1018                self.apply_tabs_layout(
1019                    children.clone(),
1020                    active_tab,
1021                    tab_bar_height,
1022                    content_padding,
1023                    parent_layout,
1024                );
1025                children
1026            }
1027            None => {
1028                // Not a docking widget, get regular children
1029                let Some(node) = self.nodes.get(&node_id) else {
1030                    return;
1031                };
1032                node.children.clone()
1033            }
1034        };
1035
1036        // Recursively process children
1037        for child_id in children_to_recurse {
1038            self.post_process_docking_layouts(child_id);
1039        }
1040    }
1041
1042    /// Apply layout to DockSplitter children.
1043    #[cfg(feature = "docking")]
1044    fn apply_splitter_layout(
1045        &mut self,
1046        children: Vec<NodeId>,
1047        direction: crate::widgets::docking::SplitDirection,
1048        split_ratio: f32,
1049        separator_size: f32,
1050        parent_layout: LayoutRect,
1051    ) {
1052        if children.len() < 2 {
1053            return;
1054        }
1055
1056        let half_sep = separator_size / 2.0;
1057
1058        match direction {
1059            crate::widgets::docking::SplitDirection::Horizontal => {
1060                // Left/Right split
1061                let split_x = parent_layout.width * split_ratio;
1062
1063                // First child (left)
1064                if let Some(node) = self.nodes.get_mut(&children[0]) {
1065                    node.layout = LayoutRect {
1066                        x: 0.0,
1067                        y: 0.0,
1068                        width: (split_x - half_sep).max(0.0),
1069                        height: parent_layout.height,
1070                    };
1071                }
1072
1073                // Second child (right)
1074                if let Some(node) = self.nodes.get_mut(&children[1]) {
1075                    node.layout = LayoutRect {
1076                        x: split_x + half_sep,
1077                        y: 0.0,
1078                        width: (parent_layout.width - split_x - half_sep).max(0.0),
1079                        height: parent_layout.height,
1080                    };
1081                }
1082            }
1083            crate::widgets::docking::SplitDirection::Vertical => {
1084                // Top/Bottom split
1085                let split_y = parent_layout.height * split_ratio;
1086
1087                // First child (top)
1088                if let Some(node) = self.nodes.get_mut(&children[0]) {
1089                    node.layout = LayoutRect {
1090                        x: 0.0,
1091                        y: 0.0,
1092                        width: parent_layout.width,
1093                        height: (split_y - half_sep).max(0.0),
1094                    };
1095                }
1096
1097                // Second child (bottom)
1098                if let Some(node) = self.nodes.get_mut(&children[1]) {
1099                    node.layout = LayoutRect {
1100                        x: 0.0,
1101                        y: split_y + half_sep,
1102                        width: parent_layout.width,
1103                        height: (parent_layout.height - split_y - half_sep).max(0.0),
1104                    };
1105                }
1106            }
1107        }
1108    }
1109
1110    /// Apply layout to DockTabs children.
1111    #[cfg(feature = "docking")]
1112    fn apply_tabs_layout(
1113        &mut self,
1114        children: Vec<NodeId>,
1115        _active_tab: usize,
1116        tab_bar_height: f32,
1117        content_padding: f32,
1118        parent_layout: LayoutRect,
1119    ) {
1120        // Content area is below the tab bar, inset by content_padding on all sides
1121        let content_layout = LayoutRect {
1122            x: content_padding,
1123            y: tab_bar_height + content_padding,
1124            width: (parent_layout.width - content_padding * 2.0).max(0.0),
1125            height: (parent_layout.height - tab_bar_height - content_padding * 2.0).max(0.0),
1126        };
1127
1128        // All tab content children get the same layout (content area)
1129        // The renderer will only show the active one
1130        for child_id in &children {
1131            if let Some(node) = self.nodes.get_mut(child_id) {
1132                node.layout = content_layout;
1133            }
1134        }
1135    }
1136
1137    /// Resolve viewport-relative units (vw, vh, vmin, vmax) and complex constraints to absolute pixels.
1138    ///
1139    /// This is called before Taffy layout computation to convert viewport units
1140    /// and complex constraints into pixel values that Taffy can understand.
1141    fn resolve_viewport_units(&mut self, viewport_size: astrelis_core::geometry::Size<f32>) {
1142        profile_scope!("resolve_viewport_units");
1143
1144        if self.viewport_constraint_nodes.is_empty() {
1145            return;
1146        }
1147
1148        let viewport = Vec2::new(viewport_size.width, viewport_size.height);
1149        let ctx = ResolveContext::viewport_only(viewport);
1150
1151        // Collect nodes to avoid borrowing issues
1152        let constraint_nodes: Vec<NodeId> =
1153            self.viewport_constraint_nodes.iter().copied().collect();
1154
1155        for node_id in constraint_nodes {
1156            if let Some(node) = self.nodes.get_mut(&node_id) {
1157                let style = node.widget.style_mut();
1158                let mut changed = false;
1159
1160                // Get the constraints box if present
1161                if let Some(ref constraints) = style.constraints {
1162                    // Resolve width
1163                    if let Some(ref constraint) = constraints.width
1164                        && constraint.needs_resolution()
1165                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1166                    {
1167                        style.layout.size.width = taffy::Dimension::Length(px);
1168                        changed = true;
1169                    }
1170
1171                    // Resolve height
1172                    if let Some(ref constraint) = constraints.height
1173                        && constraint.needs_resolution()
1174                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1175                    {
1176                        style.layout.size.height = taffy::Dimension::Length(px);
1177                        changed = true;
1178                    }
1179
1180                    // Resolve min_width
1181                    if let Some(ref constraint) = constraints.min_width
1182                        && constraint.needs_resolution()
1183                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1184                    {
1185                        style.layout.min_size.width = taffy::Dimension::Length(px);
1186                        changed = true;
1187                    }
1188
1189                    // Resolve min_height
1190                    if let Some(ref constraint) = constraints.min_height
1191                        && constraint.needs_resolution()
1192                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1193                    {
1194                        style.layout.min_size.height = taffy::Dimension::Length(px);
1195                        changed = true;
1196                    }
1197
1198                    // Resolve max_width
1199                    if let Some(ref constraint) = constraints.max_width
1200                        && constraint.needs_resolution()
1201                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1202                    {
1203                        style.layout.max_size.width = taffy::Dimension::Length(px);
1204                        changed = true;
1205                    }
1206
1207                    // Resolve max_height
1208                    if let Some(ref constraint) = constraints.max_height
1209                        && constraint.needs_resolution()
1210                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1211                    {
1212                        style.layout.max_size.height = taffy::Dimension::Length(px);
1213                        changed = true;
1214                    }
1215
1216                    // Resolve padding
1217                    if let Some(ref padding) = constraints.padding
1218                        && padding.iter().any(|c| c.needs_resolution())
1219                    {
1220                        if let Some(px) = ConstraintResolver::resolve(&padding[0], &ctx) {
1221                            style.layout.padding.left = taffy::LengthPercentage::Length(px);
1222                            changed = true;
1223                        }
1224                        if let Some(px) = ConstraintResolver::resolve(&padding[1], &ctx) {
1225                            style.layout.padding.top = taffy::LengthPercentage::Length(px);
1226                            changed = true;
1227                        }
1228                        if let Some(px) = ConstraintResolver::resolve(&padding[2], &ctx) {
1229                            style.layout.padding.right = taffy::LengthPercentage::Length(px);
1230                            changed = true;
1231                        }
1232                        if let Some(px) = ConstraintResolver::resolve(&padding[3], &ctx) {
1233                            style.layout.padding.bottom = taffy::LengthPercentage::Length(px);
1234                            changed = true;
1235                        }
1236                    }
1237
1238                    // Resolve margin
1239                    if let Some(ref margin) = constraints.margin
1240                        && margin.iter().any(|c| c.needs_resolution())
1241                    {
1242                        if let Some(px) = ConstraintResolver::resolve(&margin[0], &ctx) {
1243                            style.layout.margin.left = taffy::LengthPercentageAuto::Length(px);
1244                            changed = true;
1245                        }
1246                        if let Some(px) = ConstraintResolver::resolve(&margin[1], &ctx) {
1247                            style.layout.margin.top = taffy::LengthPercentageAuto::Length(px);
1248                            changed = true;
1249                        }
1250                        if let Some(px) = ConstraintResolver::resolve(&margin[2], &ctx) {
1251                            style.layout.margin.right = taffy::LengthPercentageAuto::Length(px);
1252                            changed = true;
1253                        }
1254                        if let Some(px) = ConstraintResolver::resolve(&margin[3], &ctx) {
1255                            style.layout.margin.bottom = taffy::LengthPercentageAuto::Length(px);
1256                            changed = true;
1257                        }
1258                    }
1259
1260                    // Resolve gap
1261                    if let Some(ref constraint) = constraints.gap
1262                        && constraint.needs_resolution()
1263                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1264                    {
1265                        style.layout.gap.width = taffy::LengthPercentage::Length(px);
1266                        style.layout.gap.height = taffy::LengthPercentage::Length(px);
1267                        changed = true;
1268                    }
1269
1270                    // Resolve flex_basis
1271                    if let Some(ref constraint) = constraints.flex_basis
1272                        && constraint.needs_resolution()
1273                        && let Some(px) = ConstraintResolver::resolve(constraint, &ctx)
1274                    {
1275                        style.layout.flex_basis = taffy::Dimension::Length(px);
1276                        changed = true;
1277                    }
1278                }
1279
1280                // Update Taffy with the resolved style if anything changed
1281                if changed {
1282                    let taffy_node = node.taffy_node;
1283                    let layout_style = style.layout.clone();
1284                    self.taffy.set_style(taffy_node, layout_style).ok();
1285                    self.taffy.mark_dirty(taffy_node).ok();
1286                }
1287            }
1288        }
1289    }
1290
1291    /// Mark all viewport-constraint nodes as needing layout.
1292    ///
1293    /// Called when viewport size changes to trigger re-resolution of viewport units.
1294    pub fn mark_viewport_dirty(&mut self) {
1295        let updates: Vec<(NodeId, DirtyFlags)> = self
1296            .viewport_constraint_nodes
1297            .iter()
1298            .map(|&id| (id, DirtyFlags::LAYOUT))
1299            .collect();
1300        self.mark_dirty_batch(&updates);
1301    }
1302
1303    /// Mark all nodes with the given dirty flags.
1304    pub fn mark_all_dirty(&mut self, flags: DirtyFlags) {
1305        self.mark_all_dirty_uniform(flags);
1306    }
1307
1308    /// Cache layout results from Taffy into our nodes.
1309    #[allow(dead_code)]
1310    fn cache_layouts(&mut self) {
1311        let node_ids: Vec<NodeId> = self.nodes.keys().copied().collect();
1312
1313        for node_id in node_ids {
1314            if let Some(node) = self.nodes.get(&node_id)
1315                && let Ok(layout) = self.taffy.layout(node.taffy_node)
1316            {
1317                let layout_rect = LayoutRect {
1318                    x: layout.location.x,
1319                    y: layout.location.y,
1320                    width: layout.size.width,
1321                    height: layout.size.height,
1322                };
1323
1324                if let Some(node) = self.nodes.get_mut(&node_id) {
1325                    node.layout = layout_rect;
1326                }
1327            }
1328        }
1329    }
1330
1331    /// Update layout for a specific subtree from Taffy results.
1332    ///
1333    /// Propagates inherited visual properties (z_index, opacity, translate, scale)
1334    /// from parent to children during the traversal.
1335    fn update_subtree_layout(&mut self, root_id: NodeId) {
1336        // Stack carries: (node_id, parent_z, parent_opacity, parent_translate, parent_scale)
1337        let mut stack: Vec<(NodeId, u16, f32, Vec2, Vec2)> =
1338            vec![(root_id, 0, 1.0, Vec2::ZERO, Vec2::ONE)];
1339
1340        while let Some((node_id, parent_z, parent_opacity, parent_translate, parent_scale)) =
1341            stack.pop()
1342        {
1343            // Get node's style offsets and children before any mutable borrows
1344            let (z_offset, opacity, translate, node_scale, children) =
1345                if let Some(node) = self.nodes.get(&node_id) {
1346                    let s = node.widget.style();
1347                    (
1348                        s.z_index,
1349                        s.opacity,
1350                        s.translate,
1351                        s.scale,
1352                        node.children.clone(),
1353                    )
1354                } else {
1355                    continue;
1356                };
1357
1358            // Compute accumulated inherited visual properties
1359            let computed_z = parent_z.saturating_add(z_offset);
1360            let computed_opacity = parent_opacity * opacity;
1361            let computed_translate = parent_translate + translate;
1362            let computed_scale =
1363                Vec2::new(parent_scale.x * node_scale.x, parent_scale.y * node_scale.y);
1364
1365            // Update this node's layout and computed visuals
1366            if let Some(node) = self.nodes.get_mut(&node_id)
1367                && let Ok(layout) = self.taffy.layout(node.taffy_node)
1368            {
1369                node.layout = LayoutRect {
1370                    x: layout.location.x,
1371                    y: layout.location.y,
1372                    width: layout.size.width,
1373                    height: layout.size.height,
1374                };
1375                node.computed_z_index = computed_z;
1376                node.computed_opacity = computed_opacity;
1377                node.computed_translate = computed_translate;
1378                node.computed_scale = computed_scale;
1379            }
1380
1381            // Push children with accumulated inherited visuals
1382            for child_id in children {
1383                stack.push((
1384                    child_id,
1385                    computed_z,
1386                    computed_opacity,
1387                    computed_translate,
1388                    computed_scale,
1389                ));
1390            }
1391        }
1392    }
1393
1394    /// Mark a node and all its descendants with the Z_INDEX dirty flag.
1395    ///
1396    /// Z_INDEX propagates DOWN to children (unlike LAYOUT flags which propagate up).
1397    /// This is called when a node's z_index style property changes, since the
1398    /// computed_z_index of all descendants depends on ancestor z_index values.
1399    pub fn mark_z_index_dirty(&mut self, node_id: NodeId) {
1400        self.mark_descendants_dirty(node_id, DirtyFlags::Z_INDEX);
1401    }
1402
1403    /// Mark a node and all its descendants with the OPACITY dirty flag.
1404    ///
1405    /// OPACITY propagates DOWN to children because computed_opacity depends
1406    /// on ancestor opacity values.
1407    pub fn mark_opacity_dirty(&mut self, node_id: NodeId) {
1408        self.mark_descendants_dirty(node_id, DirtyFlags::OPACITY);
1409    }
1410
1411    /// Mark a node and all its descendants with the TRANSFORM dirty flag.
1412    ///
1413    /// TRANSFORM propagates DOWN to children because computed_translate/scale
1414    /// depend on ancestor transform values.
1415    pub fn mark_transform_dirty(&mut self, node_id: NodeId) {
1416        self.mark_descendants_dirty(node_id, DirtyFlags::TRANSFORM);
1417    }
1418
1419    /// Mark a node and all its descendants with the given dirty flag.
1420    ///
1421    /// Used for flags that propagate DOWN (z_index, opacity, transform).
1422    fn mark_descendants_dirty(&mut self, node_id: NodeId, flag: DirtyFlags) {
1423        let mut stack = vec![node_id];
1424        while let Some(id) = stack.pop() {
1425            self.dirty_nodes.insert(id);
1426            if let Some(node) = self.nodes.get_mut(&id) {
1427                let old_flags = node.dirty_flags;
1428                node.dirty_flags |= flag;
1429                self.dirty_counters.on_mark(old_flags, flag);
1430                node.paint_version = node.paint_version.wrapping_add(1);
1431                stack.extend(node.children.iter().copied());
1432            }
1433        }
1434    }
1435
1436    /// Clear the entire tree.
1437    pub fn clear(&mut self) {
1438        self.nodes.clear();
1439        self.taffy.clear();
1440        self.root = None;
1441        self.next_id = 0;
1442        self.dirty_nodes.clear();
1443        self.dirty_roots.clear();
1444        self.dirty_counters.reset();
1445        self.viewport_constraint_nodes.clear();
1446        self.removed_nodes.clear();
1447    }
1448
1449    /// Drain the list of removed node IDs (returns and clears the list).
1450    ///
1451    /// The renderer calls this to learn which nodes were removed since
1452    /// the last drain, allowing it to clean up stale draw commands.
1453    pub fn drain_removed_nodes(&mut self) -> Vec<NodeId> {
1454        std::mem::take(&mut self.removed_nodes)
1455    }
1456
1457    /// Check whether a node still exists in the tree.
1458    pub fn node_exists(&self, node_id: NodeId) -> bool {
1459        self.nodes.contains_key(&node_id)
1460    }
1461
1462    /// Sync a node's widget style to its Taffy layout node.
1463    ///
1464    /// Call after externally modifying a widget's style to ensure Taffy
1465    /// picks up the changes on next layout computation.
1466    pub(crate) fn sync_taffy_style(&mut self, node_id: NodeId) {
1467        if let Some(node) = self.nodes.get(&node_id) {
1468            let taffy_node = node.taffy_node;
1469            let layout_style = node.widget.style().layout.clone();
1470            self.taffy.set_style(taffy_node, layout_style).ok();
1471            self.taffy.mark_dirty(taffy_node).ok();
1472        }
1473    }
1474
1475    /// Find all nodes whose widget downcasts to the given type.
1476    ///
1477    /// Returns a vector of (NodeId, absolute layout rect) pairs for each matching widget.
1478    /// Useful for finding all DockTabs containers during cross-container drag operations.
1479    pub fn find_widgets_with_layout<T: 'static>(&self) -> Vec<(NodeId, LayoutRect)> {
1480        let mut results = Vec::new();
1481        if let Some(root) = self.root {
1482            self.find_widgets_recursive::<T>(root, Vec2::ZERO, &mut results);
1483        }
1484        results
1485    }
1486
1487    /// Recursively search for widgets of a given type.
1488    fn find_widgets_recursive<T: 'static>(
1489        &self,
1490        node_id: NodeId,
1491        parent_offset: Vec2,
1492        results: &mut Vec<(NodeId, LayoutRect)>,
1493    ) {
1494        let Some(node) = self.nodes.get(&node_id) else {
1495            return;
1496        };
1497
1498        let abs_x = parent_offset.x + node.layout.x;
1499        let abs_y = parent_offset.y + node.layout.y;
1500
1501        // Check if this widget matches the type
1502        if node.widget.as_any().downcast_ref::<T>().is_some() {
1503            results.push((
1504                node_id,
1505                LayoutRect {
1506                    x: abs_x,
1507                    y: abs_y,
1508                    width: node.layout.width,
1509                    height: node.layout.height,
1510                },
1511            ));
1512        }
1513
1514        // Recurse into children
1515        let children = node.children.clone();
1516        let offset = Vec2::new(abs_x, abs_y);
1517        for child_id in children {
1518            self.find_widgets_recursive::<T>(child_id, offset, results);
1519        }
1520    }
1521
1522    /// Iterate over all nodes.
1523    pub fn iter(&self) -> impl Iterator<Item = (NodeId, &UiNode)> {
1524        self.nodes.iter().map(|(id, node)| (*id, node))
1525    }
1526
1527    /// Iterate over all nodes mutably.
1528    pub fn iter_mut(&mut self) -> impl Iterator<Item = (NodeId, &mut UiNode)> {
1529        self.nodes.iter_mut().map(|(id, node)| (*id, node))
1530    }
1531
1532    /// Get all node IDs.
1533    pub fn node_ids(&self) -> Vec<NodeId> {
1534        self.nodes.keys().copied().collect()
1535    }
1536
1537    /// Update a widget's style and mark tree dirty.
1538    pub fn update_style(&mut self, node_id: NodeId, style: Style) {
1539        if let Some(node) = self.nodes.get_mut(&node_id) {
1540            *node.widget.style_mut() = style.clone();
1541            self.taffy.set_style(node.taffy_node, style.layout).ok();
1542            self.mark_dirty_flags(node_id, DirtyFlags::LAYOUT);
1543        }
1544    }
1545
1546    /// Remove a node and all its descendants from the tree.
1547    ///
1548    /// This properly cleans up both the UI tree and the underlying Taffy layout tree.
1549    /// If the node has a parent, it will be removed from the parent's children list.
1550    ///
1551    /// # Arguments
1552    ///
1553    /// * `node_id` - The node to remove
1554    ///
1555    /// # Returns
1556    ///
1557    /// `true` if the node was removed, `false` if it didn't exist
1558    ///
1559    /// # Example
1560    ///
1561    /// ```ignore
1562    /// // Remove a node from virtual scrolling when it scrolls out of view
1563    /// if tree.remove_node(old_node_id) {
1564    ///     // Node was successfully removed
1565    /// }
1566    /// ```
1567    pub fn remove_node(&mut self, node_id: NodeId) -> bool {
1568        // Check if node exists
1569        if !self.nodes.contains_key(&node_id) {
1570            return false;
1571        }
1572
1573        // Collect all descendant nodes to remove (depth-first traversal)
1574        let mut to_remove = Vec::new();
1575        let mut stack = vec![node_id];
1576
1577        while let Some(id) = stack.pop() {
1578            to_remove.push(id);
1579            if let Some(node) = self.nodes.get(&id) {
1580                stack.extend(node.children.iter().copied());
1581            }
1582        }
1583
1584        // Remove from parent's children list
1585        if let Some(node) = self.nodes.get(&node_id)
1586            && let Some(parent_id) = node.parent
1587            && let Some(parent_node) = self.nodes.get_mut(&parent_id)
1588        {
1589            parent_node.children.retain(|&child| child != node_id);
1590            // Mark parent dirty since children changed
1591            self.mark_dirty_flags(parent_id, DirtyFlags::CHILDREN_ORDER);
1592        }
1593
1594        // Remove all nodes (children first, then parent)
1595        for id in to_remove.iter().rev() {
1596            if let Some(node) = self.nodes.shift_remove(id) {
1597                // Remove from Taffy
1598                self.taffy.remove(node.taffy_node).ok();
1599                // Update dirty counters before removing from dirty tracking
1600                if !node.dirty_flags.is_empty() {
1601                    self.dirty_counters.on_clear(node.dirty_flags);
1602                }
1603                // Remove from dirty tracking
1604                self.dirty_nodes.remove(id);
1605                self.dirty_roots.remove(id);
1606                // Remove from viewport constraint tracking
1607                self.viewport_constraint_nodes.remove(id);
1608                // Track removal for renderer cleanup
1609                self.removed_nodes.push(*id);
1610            }
1611        }
1612
1613        // If we removed the root, clear it
1614        if self.root == Some(node_id) {
1615            self.root = None;
1616        }
1617
1618        true
1619    }
1620
1621    /// Remove a child from a parent node without removing it from the tree.
1622    ///
1623    /// This is useful for reorganizing the tree structure without destroying nodes.
1624    ///
1625    /// # Arguments
1626    ///
1627    /// * `parent` - The parent node
1628    /// * `child` - The child to remove from the parent
1629    ///
1630    /// # Returns
1631    ///
1632    /// `true` if the child was removed from the parent, `false` otherwise
1633    pub fn remove_child(&mut self, parent: NodeId, child: NodeId) -> bool {
1634        // Get taffy nodes before any mutable borrows
1635        let (parent_taffy, child_taffy) = match (self.nodes.get(&parent), self.nodes.get(&child)) {
1636            (Some(p), Some(c)) => (p.taffy_node, c.taffy_node),
1637            _ => return false,
1638        };
1639
1640        // Check if parent has this child
1641        let had_child = self
1642            .nodes
1643            .get(&parent)
1644            .map(|p| p.children.contains(&child))
1645            .unwrap_or(false);
1646
1647        if !had_child {
1648            return false;
1649        }
1650
1651        // Remove child from parent's children list
1652        if let Some(parent_node) = self.nodes.get_mut(&parent) {
1653            parent_node.children.retain(|&c| c != child);
1654        }
1655
1656        // Update Taffy
1657        self.taffy.remove_child(parent_taffy, child_taffy).ok();
1658
1659        // Clear child's parent
1660        if let Some(child_node) = self.nodes.get_mut(&child) {
1661            child_node.parent = None;
1662        }
1663
1664        self.mark_dirty_flags(parent, DirtyFlags::CHILDREN_ORDER);
1665        true
1666    }
1667
1668    /// Apply a position offset to a node for effects like scrolling.
1669    ///
1670    /// This modifies the layout position without affecting the Taffy layout,
1671    /// useful for virtual scrolling or other transform effects.
1672    ///
1673    /// # Arguments
1674    ///
1675    /// * `node_id` - The node to offset
1676    /// * `x_offset` - X position offset in pixels
1677    /// * `y_offset` - Y position offset in pixels
1678    ///
1679    /// # Example
1680    ///
1681    /// ```ignore
1682    /// // Apply scroll offset to virtually scrolled items
1683    /// let scroll_offset = 100.0;
1684    /// tree.set_position_offset(item_node, 0.0, item_y - scroll_offset);
1685    /// ```
1686    pub fn set_position_offset(&mut self, node_id: NodeId, x_offset: f32, y_offset: f32) {
1687        if let Some(node) = self.nodes.get_mut(&node_id) {
1688            // Store the Taffy-computed position if not already stored
1689            // This assumes layout has been computed
1690            if let Ok(layout) = self.taffy.layout(node.taffy_node) {
1691                // Apply offset to the Taffy position
1692                node.layout.x = layout.location.x + x_offset;
1693                node.layout.y = layout.location.y + y_offset;
1694            }
1695        }
1696    }
1697
1698    /// Get the computed layout position without any applied offsets.
1699    ///
1700    /// This returns the position as computed by Taffy, ignoring any manual offsets.
1701    pub fn get_base_position(&self, node_id: NodeId) -> Option<(f32, f32)> {
1702        self.nodes.get(&node_id).and_then(|node| {
1703            self.taffy
1704                .layout(node.taffy_node)
1705                .ok()
1706                .map(|layout| (layout.location.x, layout.location.y))
1707        })
1708    }
1709}
1710
1711impl Default for UiTree {
1712    fn default() -> Self {
1713        Self::new()
1714    }
1715}