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}