Skip to main content

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}