ailloli_ui_runtime/element/
element_tree.rs

1//! Indexed retained element tree and its mutation/query operations.
2
3use std::collections::{HashMap, HashSet};
4
5use ailloli_ui_core::style::{FlexItemStyle, LayoutSizeHint};
6use ailloli_ui_core::{ElementId, WidgetId};
7
8use super::element_node::LayoutCacheKey;
9use super::{DirtyFlags, Element, ElementKind, Key};
10use crate::app::diagnostics::ElementTreeDiagnostics;
11use crate::app::ElementTreeDiagnosticsSnapshot;
12#[cfg(feature = "devtools")]
13use crate::layout::LayoutDebugInfo;
14use crate::layout::LayoutResult;
15
16/// Error resolving a view key (`View::key`) in the element tree.
17///
18/// The requested key is owned by the error so it remains useful after the tree
19/// borrow ends. Duplicate counts include all string/static keys equal to the
20/// requested text; numeric keys are never matched by this API.
21///
22/// # Examples
23///
24/// ```
25/// use ailloli_ui_runtime::element::ViewKeyResolveError;
26/// let error = ViewKeyResolveError::Missing { key: String::from("search") };
27/// assert!(matches!(error, ViewKeyResolveError::Missing { key } if key == "search"));
28/// ```
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ViewKeyResolveError {
31    /// No retained string/static key matched.
32    Missing {
33        /// Requested key text.
34        key: String,
35    },
36    /// More than one retained string/static key matched.
37    Duplicate {
38        /// Requested key text.
39        key: String,
40        /// Number of matching retained elements; always at least two.
41        count: usize,
42    },
43}
44
45/// Retained element tree produced by reconciliation.
46///
47/// Element and widget IDs are monotonically allocated within one tree and
48/// start at one. Storage uses a hash map, so iteration order is unspecified.
49/// Relationship fields are not automatically repaired when callers use the
50/// low-level mutation/removal APIs.
51///
52/// # Examples
53///
54/// ```
55/// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
56/// let mut tree = ElementTree::<()>::new();
57/// let root = tree.create_element(ElementKind::Empty, None, None);
58/// assert_eq!(tree.root(), Some(root));
59/// assert!(tree.get(root).unwrap().dirty.layout);
60/// ```
61pub struct ElementTree<A> {
62    /// Last allocated monotone element identifier; zero is never emitted.
63    next_element: u64,
64    /// Last allocated monotone widget identifier; zero is never emitted.
65    next_widget: u64,
66    /// Retained elements indexed by stable element identity.
67    elements: HashMap<ElementId, Element<A>>,
68    /// Optional root element; low-level mutations do not repair it automatically.
69    root: Option<ElementId>,
70    /// Cumulative diagnostic counters for tree mutations and anomalies.
71    diagnostics: ElementTreeDiagnostics,
72}
73
74/// Implements the `Default` contract for `ElementTree<A>`.
75impl<A> Default for ElementTree<A> {
76    /// Constructs the documented default value.
77    fn default() -> Self {
78        Self {
79            next_element: 0,
80            next_widget: 0,
81            elements: HashMap::new(),
82            root: None,
83            diagnostics: ElementTreeDiagnostics::default(),
84        }
85    }
86}
87
88/// Provides the operations defined for `ElementTree<A>`.
89impl<A> ElementTree<A> {
90    /// Creates an empty tree with zeroed ID counters and diagnostics.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use ailloli_ui_runtime::element::ElementTree;
96    /// let tree = ElementTree::<()>::new();
97    /// assert!(tree.root().is_none());
98    /// assert_eq!(tree.iter_elements().count(), 0);
99    /// ```
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Returns the first element ever created as the retained root.
105    ///
106    /// `None` means no element has yet been created. Low-level
107    /// [`Self::remove_element`] does not clear this field, so removing the root
108    /// can leave a stale `Some(id)`; reconciliation owns normal root lifetime.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
114    /// let mut tree = ElementTree::<()>::new();
115    /// let id = tree.create_element(ElementKind::Empty, None, None);
116    /// assert_eq!(tree.root(), Some(id));
117    /// ```
118    pub fn root(&self) -> Option<ElementId> {
119        self.root
120    }
121
122    /// Borrows an element by ID, returning `None` for unknown or removed IDs.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use ailloli_ui_core::ElementId;
128    /// use ailloli_ui_runtime::element::ElementTree;
129    /// let tree = ElementTree::<()> ::new();
130    /// assert!(tree.get(ElementId(1)).is_none());
131    /// ```
132    pub fn get(&self, id: ElementId) -> Option<&Element<A>> {
133        self.elements.get(&id)
134    }
135
136    /// Mutably borrows an element by ID.
137    ///
138    /// Direct edits can bypass revision, cache, parent/child, and diagnostic
139    /// invariants. `None` is returned for unknown or removed IDs.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree, Key};
145    /// let mut tree = ElementTree::<()>::new();
146    /// let id = tree.create_element(ElementKind::Empty, None, None);
147    /// tree.get_mut(id).unwrap().key = Some(Key::Static("root"));
148    /// assert_eq!(tree.get(id).unwrap().key, Some(Key::Static("root")));
149    /// ```
150    pub fn get_mut(&mut self, id: ElementId) -> Option<&mut Element<A>> {
151        self.elements.get_mut(&id)
152    }
153
154    /// Allocates and inserts one dirty retained element.
155    ///
156    /// Element and widget counters are incremented independently and the new
157    /// IDs start at one. The first inserted element becomes `root`; later
158    /// insertions do not change it. The element starts layout+paint dirty with
159    /// layout/topology revisions `1`, no cached layout/bounds, default view
160    /// metadata, and caller-supplied `parent` stored verbatim. It is not
161    /// automatically added to the parent's child list.
162    ///
163    /// # Panics
164    ///
165    /// Panics before changing either counter if either `u64` identifier space
166    /// is exhausted. Identifiers are never wrapped or silently reused.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree, Key};
172    /// let mut tree = ElementTree::<()>::new();
173    /// let id = tree.create_element(ElementKind::Empty, Some(Key::U64(9)), None);
174    /// let element = tree.get(id).unwrap();
175    /// assert_eq!(id.0, 1);
176    /// assert_eq!(element.key, Some(Key::U64(9)));
177    /// ```
178    pub fn create_element(
179        &mut self,
180        kind: ElementKind<A>,
181        key: Option<Key>,
182        parent: Option<ElementId>,
183    ) -> ElementId {
184        let next_element = self
185            .next_element
186            .checked_add(1)
187            .expect("element identity space exhausted");
188        let next_widget = self
189            .next_widget
190            .checked_add(1)
191            .expect("widget identity space exhausted");
192        self.next_element = next_element;
193        self.next_widget = next_widget;
194        let id = ElementId(next_element);
195        let widget_id = WidgetId(next_widget);
196        let el = Element {
197            id,
198            widget_id,
199            mount_generation: crate::component::reactive::MountGeneration::INITIAL,
200            key,
201            kind,
202            dirty: DirtyFlags::layout(),
203            parent,
204            children: Vec::new(),
205            layout: None,
206            layout_cache_key: None,
207            layout_reactive_dependencies: Default::default(),
208            layout_commit_reactive_dependencies: Default::default(),
209            committed_layout_generation: None,
210            committed_layout_attempt: None,
211            measurement_layout: None,
212            measurement_layout_cache_key: None,
213            measurement_reactive_dependencies: Default::default(),
214            layout_revision: 1,
215            topology_revision: 1,
216            layout_changed: true,
217            layout_callback_executed: false,
218            commit_dirty: true,
219            committed_bounds: None,
220            flex_item: FlexItemStyle::default(),
221            size_hint: LayoutSizeHint::default(),
222            #[cfg(feature = "devtools")]
223            layout_debug: None,
224        };
225        self.elements.insert(id, el);
226        if self.root.is_none() {
227            self.root = Some(id);
228        }
229        id
230    }
231
232    /// Replaces a parent's ordered direct-child IDs when the vector differs.
233    ///
234    /// A real change increments topology and layout revisions with wrapping but
235    /// reserves zero as a sentinel, clears the layout cache key, marks layout
236    /// and paint dirty, and requests commit. Equal vectors and unknown parent
237    /// IDs are no-ops. This method does not update each child's `parent`, reject
238    /// duplicates, or verify that IDs exist.
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
244    /// let mut tree = ElementTree::<()>::new();
245    /// let parent = tree.create_element(ElementKind::Empty, None, None);
246    /// let child = tree.create_element(ElementKind::Empty, None, Some(parent));
247    /// tree.set_children(parent, vec![child]);
248    /// assert_eq!(tree.children_of(parent), &[child]);
249    /// ```
250    pub fn set_children(&mut self, parent: ElementId, children: Vec<ElementId>) {
251        if let Some(p) = self.elements.get_mut(&parent) {
252            if p.children != children {
253                p.children = children;
254                p.topology_revision = p.topology_revision.wrapping_add(1).max(1);
255                p.layout_revision = p.layout_revision.wrapping_add(1).max(1);
256                p.layout_cache_key = None;
257                p.measurement_layout = None;
258                p.measurement_layout_cache_key = None;
259                p.measurement_reactive_dependencies = Default::default();
260                p.dirty = DirtyFlags::layout();
261                p.commit_dirty = true;
262            }
263        }
264    }
265
266    /// Borrows direct-child IDs in retained order.
267    ///
268    /// Unknown parent IDs return a shared empty slice, making missing and
269    /// childless parents indistinguishable through this accessor.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use ailloli_ui_core::ElementId;
275    /// use ailloli_ui_runtime::element::ElementTree;
276    /// let tree = ElementTree::<()>::new();
277    /// assert_eq!(tree.children_of(ElementId(99)), &[]);
278    /// ```
279    pub fn children_of(&self, parent: ElementId) -> &[ElementId] {
280        self.elements
281            .get(&parent)
282            .map(|e| e.children.as_slice())
283            .unwrap_or(&[])
284    }
285
286    /// Returns an element's stored parent ID.
287    ///
288    /// `None` represents an unknown element, a root, or a detached element.
289    /// This accessor does not verify that the returned parent still exists.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
295    /// let mut tree = ElementTree::<()>::new();
296    /// let parent = tree.create_element(ElementKind::Empty, None, None);
297    /// let child = tree.create_element(ElementKind::Empty, None, Some(parent));
298    /// assert_eq!(tree.parent_of(child), Some(parent));
299    /// ```
300    pub fn parent_of(&self, id: ElementId) -> Option<ElementId> {
301        self.elements.get(&id).and_then(|e| e.parent)
302    }
303
304    /// Returns `true` if `ancestor` is an ancestor of `descendant` (inclusive).
305    ///
306    /// Equal IDs return `true` even if no such element exists. Otherwise the
307    /// method follows stored parent pointers until `None`; malformed parent
308    /// cycles that do not contain `ancestor` cause an infinite loop.
309    ///
310    /// # Examples
311    ///
312    /// ```
313    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
314    /// let mut tree = ElementTree::<()>::new();
315    /// let root = tree.create_element(ElementKind::Empty, None, None);
316    /// let child = tree.create_element(ElementKind::Empty, None, Some(root));
317    /// assert!(tree.is_ancestor_of(root, child));
318    /// assert!(!tree.is_ancestor_of(child, root));
319    /// ```
320    pub fn is_ancestor_of(&self, ancestor: ElementId, mut descendant: ElementId) -> bool {
321        loop {
322            if descendant == ancestor {
323                return true;
324            }
325            descendant = match self.parent_of(descendant) {
326                Some(p) => p,
327                None => return false,
328            };
329        }
330    }
331
332    /// Stores a layout and clears only layout and paint dirty flags.
333    ///
334    /// Unknown IDs are ignored. This low-level setter leaves input dirtiness,
335    /// cache identity, layout-change/commit flags, and committed bounds
336    /// untouched; normal layout should use [`crate::layout::LayoutEngine`].
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
342    /// use ailloli_ui_runtime::layout::LayoutResult;
343    /// let mut tree = ElementTree::<()>::new();
344    /// let id = tree.create_element(ElementKind::Empty, None, None);
345    /// tree.set_layout(id, LayoutResult::empty());
346    /// assert!(tree.get(id).unwrap().layout.is_some());
347    /// assert!(!tree.get(id).unwrap().dirty.layout);
348    /// ```
349    pub fn set_layout(&mut self, id: ElementId, layout: LayoutResult) {
350        if let Some(e) = self.elements.get_mut(&id) {
351            e.layout = Some(layout);
352            e.layout_reactive_dependencies = Default::default();
353            e.layout_commit_reactive_dependencies = Default::default();
354            e.committed_layout_generation = Some(e.mount_generation());
355            e.committed_layout_attempt = None;
356            e.layout_callback_executed = false;
357            e.measurement_layout = None;
358            e.measurement_layout_cache_key = None;
359            e.measurement_reactive_dependencies = Default::default();
360            e.dirty.layout = false;
361            e.dirty.paint = false;
362        }
363    }
364
365    /// Stores a pass-specific layout with its cache identity.
366    ///
367    /// Measurement results use a separate speculative cache and never replace
368    /// authoritative geometry or change dirty/commit state. Committed geometry
369    /// is compared to the previous result without considering its artifact;
370    /// that path clears layout/paint dirtiness and preserves input dirtiness.
371    /// Unknown IDs are ignored.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use ailloli_ui_core::{Constraints, Scale};
377    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
378    /// use ailloli_ui_runtime::layout::{LayoutCtx, LayoutEngine};
379    /// let mut tree = ElementTree::<()>::new();
380    /// let id = tree.create_element(ElementKind::Empty, None, None);
381    /// let mut ctx = LayoutCtx::new(Scale::new(1.0));
382    /// LayoutEngine::new(&mut tree).layout_element(&mut ctx, id, Constraints::tight(1.0, 1.0));
383    /// assert!(tree.get(id).unwrap().layout.is_some());
384    /// ```
385    pub(crate) fn set_layout_with_cache_key(
386        &mut self,
387        id: ElementId,
388        layout: LayoutResult,
389        cache_key: LayoutCacheKey,
390        dependencies: crate::component::reactive::ReactiveReadSet,
391        attempt_token: crate::layout::LayoutAttemptToken,
392        callback_executed: bool,
393    ) {
394        if let Some(element) = self.elements.get_mut(&id) {
395            if cache_key.layout_pass.is_measure() {
396                element.measurement_layout = Some(layout);
397                element.measurement_layout_cache_key = Some(cache_key);
398                element.measurement_reactive_dependencies = dependencies;
399                return;
400            }
401            element.layout_changed = element
402                .layout
403                .as_ref()
404                .is_none_or(|previous| !previous.geometry_eq(&layout));
405            element.layout = Some(layout);
406            element.layout_cache_key = Some(cache_key);
407            element.layout_reactive_dependencies = dependencies;
408            element.committed_layout_generation = Some(element.mount_generation());
409            // A final cache hit ran no callback in this attempt. Exposing the
410            // current token to a bounds-only hook would let pending state from
411            // an earlier superseded callback publish against the cached result.
412            element.committed_layout_attempt = callback_executed.then_some(attempt_token);
413            element.dirty.layout = false;
414            element.dirty.paint = false;
415            element.layout_callback_executed = callback_executed;
416            element.commit_dirty |= element.layout_changed || callback_executed;
417        }
418    }
419
420    /// Marks only an existing element's paint flag.
421    ///
422    /// Unknown IDs are no-ops and no revision is changed.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// use ailloli_ui_runtime::element::DirtyFlags;
428    /// let flags = DirtyFlags::paint();
429    /// assert!(!flags.layout && flags.paint);
430    /// ```
431    pub(crate) fn mark_paint_dirty(&mut self, id: ElementId) {
432        if let Some(element) = self.elements.get_mut(&id) {
433            element.dirty.paint = true;
434        }
435    }
436
437    /// Marks one existing element layout+paint dirty and requests commit.
438    ///
439    /// Its layout revision wraps while reserving zero. The cache key is not
440    /// cleared here because the dirty flag prevents a hit. Ancestors and
441    /// siblings are untouched; unknown IDs are no-ops.
442    ///
443    /// # Examples
444    ///
445    /// ```
446    /// use ailloli_ui_runtime::element::DirtyFlags;
447    /// let flags = DirtyFlags::layout();
448    /// assert!(flags.layout && flags.paint);
449    /// ```
450    pub(crate) fn mark_element_layout_dirty(&mut self, id: ElementId) {
451        if let Some(element) = self.elements.get_mut(&id) {
452            element.dirty.layout = true;
453            element.dirty.paint = true;
454            element.layout_revision = element.layout_revision.wrapping_add(1).max(1);
455            element.commit_dirty = true;
456        }
457    }
458
459    /// Marks one layout root and its ancestor chain without touching siblings.
460    ///
461    /// Each visited ID records a propagation diagnostic. Existing nodes become
462    /// layout+paint dirty, get a nonzero wrapping layout revision, and request
463    /// commit. Traversal stops at a missing node, a stored `None` parent, or a
464    /// node already visited in the same batch.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// use ailloli_ui_runtime::element::DirtyFlags;
470    /// // Propagated nodes receive this layout-and-paint combination.
471    /// assert_eq!(DirtyFlags::layout(), DirtyFlags { layout: true, paint: true, input: false });
472    /// ```
473    pub(crate) fn mark_layout_path_dirty(&mut self, id: ElementId) {
474        let _ = self.mark_layout_paths_dirty([id]);
475    }
476
477    /// Marks several layout roots and the union of their ancestor chains.
478    ///
479    /// Every retained node in the union is dirtied, revisioned, and counted at
480    /// most once. Reaching a node already visited by an earlier root terminates
481    /// that path because all of its ancestors were already processed. Missing
482    /// roots are diagnosed once and malformed parent cycles terminate safely.
483    /// The returned set lets callers avoid separately dirtying exact nodes that
484    /// were already part of a propagated path.
485    pub(crate) fn mark_layout_paths_dirty(
486        &mut self,
487        roots: impl IntoIterator<Item = ElementId>,
488    ) -> HashSet<ElementId> {
489        let mut visited = HashSet::new();
490        for mut id in roots {
491            loop {
492                if !visited.insert(id) {
493                    break;
494                }
495                self.diagnostics.layout_propagation(id);
496                let parent = if let Some(element) = self.elements.get_mut(&id) {
497                    element.dirty.layout = true;
498                    element.dirty.paint = true;
499                    element.layout_revision = element.layout_revision.wrapping_add(1).max(1);
500                    element.commit_dirty = true;
501                    element.parent
502                } else {
503                    None
504                };
505                let Some(next) = parent else {
506                    break;
507                };
508                id = next;
509            }
510        }
511        visited
512    }
513
514    /// Replaces flex-item and declarative size metadata without invalidation.
515    ///
516    /// Unknown IDs are ignored. Reconciliation separately invalidates the
517    /// element before using this helper.
518    ///
519    /// # Examples
520    ///
521    /// ```
522    /// use ailloli_ui_core::style::{FlexItemStyle, LayoutSizeHint};
523    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
524    /// let mut tree = ElementTree::<()>::new();
525    /// let id = tree.create_element(ElementKind::Empty, None, None);
526    /// let flex = FlexItemStyle { flex_grow: 2.0, ..Default::default() };
527    /// tree.set_view_metadata(id, flex, LayoutSizeHint::default());
528    /// assert_eq!(tree.get(id).unwrap().flex_item.flex_grow, 2.0);
529    /// ```
530    pub fn set_view_metadata(
531        &mut self,
532        id: ElementId,
533        flex_item: FlexItemStyle,
534        size_hint: LayoutSizeHint,
535    ) {
536        if let Some(e) = self.elements.get_mut(&id) {
537            e.flex_item = flex_item;
538            e.size_hint = size_hint;
539        }
540    }
541
542    #[cfg(feature = "devtools")]
543    /// Stores developer-tooling layout data for an existing element.
544    ///
545    /// Unknown IDs are ignored and no dirty/revision state changes.
546    ///
547    /// # Examples
548    ///
549    /// ```
550    /// use ailloli_ui_core::{Constraints, Size};
551    /// use ailloli_ui_runtime::layout::LayoutDebugInfo;
552    /// let debug = LayoutDebugInfo {
553    ///     constraints_in: Constraints::tight(1.0, 2.0),
554    ///     constraints_final: None,
555    ///     layout_size: Size::new(1.0, 2.0),
556    /// };
557    /// assert_eq!(debug.layout_size.h, 2.0);
558    /// ```
559    pub fn set_layout_debug(&mut self, id: ElementId, debug: LayoutDebugInfo) {
560        if let Some(e) = self.elements.get_mut(&id) {
561            e.layout_debug = Some(debug);
562        }
563    }
564
565    /// Removes and returns exactly one element.
566    ///
567    /// A removal diagnostic is recorded even when the ID is absent. This
568    /// low-level operation does not recurse, unlink parent/children, clear the
569    /// root field, or remove runtime state. Reconciliation uses a private
570    /// subtree-aware path for normal deletion.
571    ///
572    /// # Examples
573    ///
574    /// ```
575    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
576    /// let mut tree = ElementTree::<()>::new();
577    /// let id = tree.create_element(ElementKind::Empty, None, None);
578    /// assert_eq!(tree.remove_element(id).unwrap().id, id);
579    /// assert!(tree.get(id).is_none());
580    /// assert_eq!(tree.root(), Some(id)); // low-level root reference is unchanged
581    /// ```
582    pub fn remove_element(&mut self, id: ElementId) -> Option<Element<A>> {
583        self.diagnostics.remove(id);
584        self.elements.remove(&id)
585    }
586
587    /// Iterates all stored elements as `(id, reference)` pairs.
588    ///
589    /// Hash-map order is unspecified and may differ across runs. The iterator
590    /// borrows the tree and allocates nothing.
591    ///
592    /// # Examples
593    ///
594    /// ```
595    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree};
596    /// let mut tree = ElementTree::<()>::new();
597    /// tree.create_element(ElementKind::Empty, None, None);
598    /// tree.create_element(ElementKind::Empty, None, None);
599    /// assert_eq!(tree.iter_elements().count(), 2);
600    /// ```
601    pub fn iter_elements(&self) -> impl Iterator<Item = (ElementId, &Element<A>)> {
602        self.elements.iter().map(|(&id, el)| (id, el))
603    }
604
605    /// Returns a point-in-time copy of saturating per-element work counters.
606    ///
607    /// Reading diagnostics does not clear them.
608    ///
609    /// # Examples
610    ///
611    /// ```
612    /// use ailloli_ui_runtime::element::ElementTree;
613    /// let tree = ElementTree::<()>::new();
614    /// assert!(tree.diagnostics().elements.is_empty());
615    /// ```
616    pub fn diagnostics(&self) -> ElementTreeDiagnosticsSnapshot {
617        self.diagnostics.snapshot()
618    }
619
620    /// Records one component build for `id` using saturating counters.
621    ///
622    /// IDs need not currently exist in the tree.
623    ///
624    /// # Examples
625    ///
626    /// ```
627    /// use ailloli_ui_runtime::element::ElementTree;
628    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
629    /// ```
630    pub(crate) fn record_build(&self, id: ElementId) {
631        self.diagnostics.build(id);
632    }
633
634    /// Records one layout execution for `id` using saturating counters.
635    ///
636    /// # Examples
637    ///
638    /// ```
639    /// use ailloli_ui_runtime::element::ElementTree;
640    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
641    /// ```
642    pub(crate) fn record_layout(&self, id: ElementId) {
643        self.diagnostics.layout(id);
644    }
645
646    /// Records one paint traversal for `id` using saturating counters.
647    ///
648    /// # Examples
649    ///
650    /// ```
651    /// use ailloli_ui_runtime::element::ElementTree;
652    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
653    /// ```
654    pub(crate) fn record_paint(&self, id: ElementId) {
655        self.diagnostics.paint(id);
656    }
657
658    /// Records one hit-test visit for `id` using saturating counters.
659    ///
660    /// # Examples
661    ///
662    /// ```
663    /// use ailloli_ui_runtime::element::ElementTree;
664    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
665    /// ```
666    pub(crate) fn record_hit_test(&self, id: ElementId) {
667        self.diagnostics.hit_test(id);
668    }
669
670    /// Records one layout-cache hit for `id`.
671    ///
672    /// # Examples
673    ///
674    /// ```
675    /// use ailloli_ui_runtime::element::ElementTree;
676    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
677    /// ```
678    pub(crate) fn record_layout_cache_hit(&self, id: ElementId) {
679        self.diagnostics.layout_cache_hit(id);
680    }
681
682    /// Records one layout-cache miss for `id`.
683    ///
684    /// # Examples
685    ///
686    /// ```
687    /// use ailloli_ui_runtime::element::ElementTree;
688    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
689    /// ```
690    pub(crate) fn record_layout_cache_miss(&self, id: ElementId) {
691        self.diagnostics.layout_cache_miss(id);
692    }
693
694    /// Records one widget layout-commit callback for `id`.
695    ///
696    /// # Examples
697    ///
698    /// ```
699    /// use ailloli_ui_runtime::element::ElementTree;
700    /// assert!(ElementTree::<()>::new().diagnostics().elements.is_empty());
701    /// ```
702    pub(crate) fn record_layout_commit(&self, id: ElementId) {
703        self.diagnostics.layout_commit(id);
704    }
705
706    /// Resolves a unique view key in the window (at most one element per key).
707    ///
708    /// Owned [`Key::String`] and borrowed [`Key::Static`] values compare by text;
709    /// [`Key::U64`] values are ignored. Missing and duplicate matches return
710    /// typed errors. Scanning is `O(number_of_elements)` and allocates a vector
711    /// of matching IDs; hash-map iteration does not affect a unique result.
712    ///
713    /// # Errors
714    ///
715    /// Returns [`ViewKeyResolveError::Missing`] for zero matches and
716    /// [`ViewKeyResolveError::Duplicate`] with the exact count for two or more.
717    ///
718    /// # Examples
719    ///
720    /// ```
721    /// use ailloli_ui_runtime::element::{ElementKind, ElementTree, Key, ViewKeyResolveError};
722    /// let mut tree = ElementTree::<()>::new();
723    /// let id = tree.create_element(ElementKind::Empty, Some(Key::Static("search")), None);
724    /// assert_eq!(tree.resolve_element_by_view_key("search"), Ok(id));
725    /// assert!(matches!(tree.resolve_element_by_view_key("missing"),
726    ///     Err(ViewKeyResolveError::Missing { .. })));
727    /// ```
728    pub fn resolve_element_by_view_key(&self, key: &str) -> Result<ElementId, ViewKeyResolveError> {
729        let mut matches: Vec<ElementId> = Vec::new();
730        for (id, el) in self.iter_elements() {
731            let Some(ref k) = el.key else {
732                continue;
733            };
734            let hit = match k {
735                Key::String(s) => s == key,
736                Key::Static(s) => *s == key,
737                Key::U64(_) => false,
738            };
739            if hit {
740                matches.push(id);
741            }
742        }
743        match matches.len() {
744            0 => Err(ViewKeyResolveError::Missing {
745                key: key.to_string(),
746            }),
747            1 => Ok(matches[0]),
748            n => Err(ViewKeyResolveError::Duplicate {
749                key: key.to_string(),
750                count: n,
751            }),
752        }
753    }
754}
755
756#[cfg(test)]
757/// View key tests implementation details.
758mod view_key_tests {
759    use super::*;
760    use crate::element::ElementKind;
761    use std::panic::{catch_unwind, AssertUnwindSafe};
762
763    /// Implements the empty_tree_with_keyed_leaf helper used by this module.
764    fn empty_tree_with_keyed_leaf(key: &str) -> ElementTree<()> {
765        let mut tree = ElementTree::new();
766        let root = tree.create_element(ElementKind::Empty, None, None);
767        let leaf = tree.create_element(
768            ElementKind::Empty,
769            Some(Key::String(key.to_string())),
770            Some(root),
771        );
772        tree.set_children(root, vec![leaf]);
773        tree
774    }
775
776    #[test]
777    /// Verifies that resolve view key missing.
778    fn resolve_view_key_missing() {
779        let tree = empty_tree_with_keyed_leaf("a");
780        let err = tree.resolve_element_by_view_key("z").unwrap_err();
781        assert!(matches!(err, ViewKeyResolveError::Missing { .. }));
782    }
783
784    #[test]
785    /// Verifies that resolve view key unique.
786    fn resolve_view_key_unique() {
787        let tree = empty_tree_with_keyed_leaf("hello");
788        let id = tree.resolve_element_by_view_key("hello").unwrap();
789        assert!(tree.get(id).is_some());
790    }
791
792    #[test]
793    /// Verifies that resolve view key duplicate.
794    fn resolve_view_key_duplicate() {
795        let mut tree: ElementTree<()> = ElementTree::new();
796        let root = tree.create_element(ElementKind::Empty, None, None);
797        let a = tree.create_element(
798            ElementKind::Empty,
799            Some(Key::String("dup".into())),
800            Some(root),
801        );
802        let b = tree.create_element(
803            ElementKind::Empty,
804            Some(Key::String("dup".into())),
805            Some(root),
806        );
807        tree.set_children(root, vec![a, b]);
808        let err = tree.resolve_element_by_view_key("dup").unwrap_err();
809        assert!(matches!(
810            err,
811            ViewKeyResolveError::Duplicate { count: 2, .. }
812        ));
813    }
814
815    #[test]
816    /// Exhausting either identity space fails atomically instead of wrapping.
817    fn create_element_rejects_identity_exhaustion_without_partial_advance() {
818        let mut widget_exhausted: ElementTree<()> = ElementTree::new();
819        widget_exhausted.next_element = 7;
820        widget_exhausted.next_widget = u64::MAX;
821        let result = catch_unwind(AssertUnwindSafe(|| {
822            widget_exhausted.create_element(ElementKind::Empty, None, None)
823        }));
824        assert!(result.is_err());
825        assert_eq!(widget_exhausted.next_element, 7);
826        assert_eq!(widget_exhausted.next_widget, u64::MAX);
827        assert!(widget_exhausted.elements.is_empty());
828
829        let mut element_exhausted: ElementTree<()> = ElementTree::new();
830        element_exhausted.next_element = u64::MAX;
831        element_exhausted.next_widget = 9;
832        let result = catch_unwind(AssertUnwindSafe(|| {
833            element_exhausted.create_element(ElementKind::Empty, None, None)
834        }));
835        assert!(result.is_err());
836        assert_eq!(element_exhausted.next_element, u64::MAX);
837        assert_eq!(element_exhausted.next_widget, 9);
838        assert!(element_exhausted.elements.is_empty());
839    }
840}