oxiui_accessibility/pool.rs
1//! Node pool for allocation-friendly accessibility tree construction.
2//!
3//! [`NodePool`] maintains a set of active [`A11yNode`] allocations keyed by
4//! [`accesskit::NodeId`] and a free list of previously-used nodes whose memory
5//! can be reused in the next frame.
6//!
7//! # Usage pattern
8//!
9//! Each UI frame:
10//! 1. Call [`NodePool::recycle`] to move all active nodes back to the free list.
11//! 2. For each widget, call [`NodePool::alloc`] (or [`NodePool::alloc_recycled`])
12//! to place a fresh / reused [`A11yNode`] into the active map.
13//! 3. Pass the active nodes to the tree builder as usual.
14//!
15//! The pool does **not** interact with [`crate::tree::A11yTree`] directly; it is a helper
16//! for callers that build the `A11yNode` graph themselves.
17
18use std::collections::HashMap;
19
20use accesskit::NodeId;
21
22use crate::tree::{A11yNode, WidgetRole};
23
24// ── NodePool ─────────────────────────────────────────────────────────────────
25
26/// A reusable pool of [`A11yNode`] allocations, keyed by [`NodeId`].
27///
28/// Reduces per-frame heap allocations when the accessibility tree is rebuilt
29/// on every frame: rather than dropping and re-allocating every node, the pool
30/// keeps previously-allocated structs (and their internal `Vec` allocations)
31/// on a free list.
32///
33/// # Invariants
34///
35/// * A node is either *active* (in `active`) or *free* (in `free_list`), never
36/// both simultaneously.
37/// * After [`recycle`](NodePool::recycle), `active` is empty and `free_list`
38/// holds all previously-active nodes.
39/// * After [`clear`](NodePool::clear), both maps are empty.
40#[derive(Debug, Default)]
41pub struct NodePool {
42 /// Nodes currently in use, indexed by their [`NodeId`].
43 active: HashMap<NodeId, A11yNode>,
44 /// Nodes available for reuse from a previous frame.
45 free_list: Vec<A11yNode>,
46}
47
48impl NodePool {
49 /// Create a new, empty pool.
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 /// Place `node` into the active map under `id`.
55 ///
56 /// If a node with the same `id` already exists it is silently replaced and
57 /// the old node is dropped (not returned to the free list, to avoid a
58 /// same-id duplicate).
59 pub fn alloc(&mut self, id: NodeId, node: A11yNode) {
60 self.active.insert(id, node);
61 }
62
63 /// Allocate a node slot, optionally reusing memory from the free list.
64 ///
65 /// If a free node is available its struct is taken from the free list and
66 /// reset to the supplied `id`, `role`, and `label` before being placed in
67 /// the active map. Otherwise a fresh node is created.
68 ///
69 /// This is the preferred allocation path for hot paths that care about
70 /// minimising heap allocation churn.
71 pub fn alloc_recycled(
72 &mut self,
73 id: NodeId,
74 role: WidgetRole,
75 label: Option<String>,
76 ) -> &mut A11yNode {
77 let mut node = match self.free_list.pop() {
78 Some(mut recycled) => {
79 // Reset to a clean state, reusing the heap-allocated `children` Vec.
80 recycled.id = id;
81 recycled.role = role;
82 recycled.label = label;
83 recycled.children.clear();
84 recycled.props = crate::props::A11yNodeProps::default();
85 recycled.text_content = None;
86 recycled
87 }
88 None => A11yNode::simple(id, role, label),
89 };
90 // Ensure the id is correct even if the recycled node carried a different one.
91 node.id = id;
92 self.active.insert(id, node);
93 // Safety: we just inserted the value; the entry must exist.
94 self.active
95 .get_mut(&id)
96 .unwrap_or_else(|| unreachable!("just inserted"))
97 }
98
99 /// Retrieve an active node by its [`NodeId`].
100 pub fn get(&self, id: &NodeId) -> Option<&A11yNode> {
101 self.active.get(id)
102 }
103
104 /// Retrieve a mutable reference to an active node.
105 pub fn get_mut(&mut self, id: &NodeId) -> Option<&mut A11yNode> {
106 self.active.get_mut(id)
107 }
108
109 /// Move all active nodes back to the free list for reuse next frame.
110 ///
111 /// After this call `active_count() == 0` and `free_count()` reflects the
112 /// total number of recycled nodes.
113 pub fn recycle(&mut self) {
114 for (_, node) in self.active.drain() {
115 self.free_list.push(node);
116 }
117 }
118
119 /// Number of currently-active nodes.
120 pub fn active_count(&self) -> usize {
121 self.active.len()
122 }
123
124 /// Number of nodes available for reuse.
125 pub fn free_count(&self) -> usize {
126 self.free_list.len()
127 }
128
129 /// Discard all nodes (active and free).
130 pub fn clear(&mut self) {
131 self.active.clear();
132 self.free_list.clear();
133 }
134
135 /// Iterate over all active nodes.
136 pub fn iter_active(&self) -> impl Iterator<Item = (&NodeId, &A11yNode)> {
137 self.active.iter()
138 }
139}