1use std::num::NonZeroU64;
15
16use accesskit::{Node, NodeId, Rect as AccessRect, Role};
17use kurbo::{Point, Size, Vec2};
18
19pub const ROOT_NODE_ID: NodeId = NodeId(1);
22
23const SLOT_BITS: u32 = 16;
27
28pub(crate) fn compose_node_id(base: NonZeroU64, slot: u16) -> NodeId {
31 debug_assert!(
32 base.get() < (1u64 << (64 - SLOT_BITS)),
33 "semantics base id {} overflows the {}-bit base field",
34 base.get(),
35 64 - SLOT_BITS
36 );
37 NodeId((base.get() << SLOT_BITS) | slot as u64)
38}
39
40#[derive(Clone, Debug, PartialEq)]
43pub struct SemanticsUpdate {
44 pub nodes: Vec<(NodeId, Node)>,
47 pub root: NodeId,
49 pub focus: Option<NodeId>,
51}
52
53impl SemanticsUpdate {
54 pub fn focus_id(&self) -> NodeId {
57 self.focus.unwrap_or(self.root)
58 }
59}
60
61pub struct SemanticsCtx {
68 origin: Point,
69 size: Size,
70 next_id: u64,
73 next_base: u64,
76 current_base: Option<NonZeroU64>,
78 current_slot: u16,
79 nodes: Vec<(NodeId, Node)>,
80 frames: Vec<Vec<NodeId>>,
82 focus: Option<NodeId>,
83}
84
85impl SemanticsCtx {
86 pub(crate) fn new(window_size: Size, next_base: u64) -> Self {
89 Self {
90 origin: Point::ZERO,
91 size: window_size,
92 next_id: 0,
93 next_base,
94 current_base: None,
95 current_slot: 0,
96 nodes: Vec::new(),
97 frames: vec![Vec::new()],
98 focus: None,
99 }
100 }
101
102 pub fn origin(&self) -> Point {
104 self.origin
105 }
106
107 pub fn size(&self) -> Size {
109 self.size
110 }
111
112 pub(crate) fn alloc_base(&mut self) -> NonZeroU64 {
115 let id = self.next_base;
116 self.next_base += 1;
117 NonZeroU64::new(id).expect("base allocator is seeded >= 2, never zero")
118 }
119
120 pub(crate) fn next_base(&self) -> u64 {
121 self.next_base
122 }
123
124 fn alloc_id(&mut self) -> NodeId {
125 let id = NodeId(self.next_id);
126 self.next_id += 1;
127 id
128 }
129
130 fn next_node_id(&mut self) -> NodeId {
132 match self.current_base {
133 Some(base) => {
134 let slot = self.current_slot;
135 self.current_slot = self
136 .current_slot
137 .checked_add(1)
138 .expect("a single widget contributes fewer than 2^16 nodes per pod");
139 compose_node_id(base, slot)
140 }
141 None => self.alloc_id(),
142 }
143 }
144
145 fn bounds(&self) -> AccessRect {
147 AccessRect {
148 x0: self.origin.x,
149 y0: self.origin.y,
150 x1: self.origin.x + self.size.width,
151 y1: self.origin.y + self.size.height,
152 }
153 }
154
155 pub fn push_node(&mut self, role: Role, build: impl FnOnce(&mut Node)) -> NodeId {
158 let id = self.next_node_id();
159 let mut node = Node::new(role);
160 node.set_bounds(self.bounds());
161 build(&mut node);
162 self.attach(id, node);
163 id
164 }
165
166 pub fn push_container(
169 &mut self,
170 role: Role,
171 build: impl FnOnce(&mut Node),
172 visit: impl FnOnce(&mut SemanticsCtx),
173 ) -> NodeId {
174 let id = self.next_node_id();
175 self.push_container_inner(id, role, build, visit)
176 }
177
178 pub(crate) fn push_container_with_id(
181 &mut self,
182 id: NodeId,
183 role: Role,
184 build: impl FnOnce(&mut Node),
185 visit: impl FnOnce(&mut SemanticsCtx),
186 ) -> NodeId {
187 self.push_container_inner(id, role, build, visit)
188 }
189
190 fn push_container_inner(
191 &mut self,
192 id: NodeId,
193 role: Role,
194 build: impl FnOnce(&mut Node),
195 visit: impl FnOnce(&mut SemanticsCtx),
196 ) -> NodeId {
197 let mut node = Node::new(role);
198 node.set_bounds(self.bounds());
199 build(&mut node);
200 self.frames.push(Vec::new());
201 visit(self);
202 let children = self.frames.pop().expect("container frame was just pushed");
203 node.set_children(children);
204 self.attach(id, node);
205 id
206 }
207
208 fn attach(&mut self, id: NodeId, node: Node) {
209 self.frames
210 .last_mut()
211 .expect("a child frame is always open during a pass")
212 .push(id);
213 self.nodes.push((id, node));
214 }
215
216 pub fn set_focused(&mut self, id: NodeId) {
218 self.focus = Some(id);
219 }
220
221 #[cfg(test)]
224 pub(crate) fn descend(
225 &mut self,
226 child_offset: Vec2,
227 child_size: Size,
228 f: impl FnOnce(&mut Self),
229 ) {
230 let saved_origin = self.origin;
231 let saved_size = self.size;
232 self.origin = saved_origin + child_offset;
233 self.size = child_size;
234 f(self);
235 self.origin = saved_origin;
236 self.size = saved_size;
237 }
238
239 pub(crate) fn descend_into_pod(
242 &mut self,
243 base: NonZeroU64,
244 child_offset: Vec2,
245 child_size: Size,
246 f: impl FnOnce(&mut Self),
247 ) {
248 let saved_origin = self.origin;
249 let saved_size = self.size;
250 let saved_base = self.current_base;
251 let saved_slot = self.current_slot;
252 self.origin = saved_origin + child_offset;
253 self.size = child_size;
254 self.current_base = Some(base);
255 self.current_slot = 0;
256 f(self);
257 self.origin = saved_origin;
258 self.size = saved_size;
259 self.current_base = saved_base;
260 self.current_slot = saved_slot;
261 }
262
263 pub(crate) fn finish(self, root: NodeId) -> SemanticsUpdate {
264 SemanticsUpdate {
265 nodes: self.nodes,
266 root,
267 focus: self.focus,
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 const BASE_SEED: u64 = 2;
279
280 #[test]
281 fn alloc_ids_are_sequential_and_deterministic() {
282 let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), BASE_SEED);
283 assert_eq!(ctx.alloc_id(), NodeId(0));
284 assert_eq!(ctx.alloc_id(), NodeId(1));
285 assert_eq!(ctx.alloc_id(), NodeId(2));
286 }
287
288 #[test]
289 fn push_node_sets_absolute_bounds_from_origin_and_size() {
290 let mut ctx = SemanticsCtx::new(Size::new(200.0, 200.0), BASE_SEED);
291 ctx.descend(Vec2::new(10.0, 20.0), Size::new(30.0, 40.0), |ctx| {
293 let id = ctx.push_node(Role::Label, |node| node.set_label("hi"));
294 assert_eq!(id, NodeId(0));
295 });
296 let (_, node) = &ctx.nodes[0];
297 assert_eq!(
298 node.bounds(),
299 Some(AccessRect {
300 x0: 10.0,
301 y0: 20.0,
302 x1: 40.0,
303 y1: 60.0,
304 })
305 );
306 assert_eq!(node.label(), Some("hi"));
307 assert_eq!(node.role(), Role::Label);
308 }
309
310 #[test]
311 fn descend_composes_nested_origins() {
312 let mut ctx = SemanticsCtx::new(Size::new(500.0, 500.0), BASE_SEED);
315 ctx.descend(Vec2::new(100.0, 200.0), Size::new(300.0, 300.0), |ctx| {
316 ctx.descend(Vec2::new(5.0, 7.0), Size::new(10.0, 10.0), |ctx| {
317 ctx.push_node(Role::Button, |_| {});
318 });
319 });
320 let (_, node) = &ctx.nodes[0];
321 assert_eq!(
322 node.bounds(),
323 Some(AccessRect {
324 x0: 105.0,
325 y0: 207.0,
326 x1: 115.0,
327 y1: 217.0,
328 })
329 );
330 }
331
332 #[test]
333 fn push_container_collects_children_and_restores_frame() {
334 let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), BASE_SEED);
335 let container = ctx.push_container(
336 Role::ScrollView,
337 |_| {},
338 |ctx| {
339 ctx.push_node(Role::Label, |n| n.set_label("a"));
340 ctx.push_node(Role::Label, |n| n.set_label("b"));
341 },
342 );
343 assert_eq!(container, NodeId(0));
346 let update = ctx.finish(container);
347 assert_eq!(update.nodes.len(), 3);
349 let container_node = &update
350 .nodes
351 .iter()
352 .find(|(id, _)| *id == container)
353 .unwrap()
354 .1;
355 assert_eq!(container_node.children(), &[NodeId(1), NodeId(2)]);
356 }
357
358 #[test]
359 fn set_focused_records_the_focus_node() {
360 let mut ctx = SemanticsCtx::new(Size::new(10.0, 10.0), BASE_SEED);
361 let id = ctx.push_node(Role::TextInput, |_| {});
362 ctx.set_focused(id);
363 let update = ctx.finish(id);
364 assert_eq!(update.focus, Some(id));
365 }
366
367 #[test]
368 fn focus_id_defaults_to_root_when_unfocused() {
369 let mut ctx = SemanticsCtx::new(Size::new(10.0, 10.0), BASE_SEED);
370 let root = ctx.push_container(Role::Window, |_| {}, |_| {});
371 let update = ctx.finish(root);
372 assert!(update.focus.is_none());
373 assert_eq!(update.focus_id(), update.root);
375 }
376
377 #[test]
378 fn compose_node_id_is_collision_free_across_bases_and_slots() {
379 use std::collections::HashSet;
383 let mut seen = HashSet::new();
384 for base in 2u64..40 {
385 let base = NonZeroU64::new(base).unwrap();
386 for slot in 0u16..1000 {
387 let id = compose_node_id(base, slot);
388 assert!(id != ROOT_NODE_ID, "never collides with the reserved root");
389 assert!(seen.insert(id), "duplicate id for base={base} slot={slot}");
390 }
391 }
392 let a = NonZeroU64::new(2).unwrap();
394 let b = NonZeroU64::new(3).unwrap();
395 assert!(compose_node_id(a, u16::MAX).0 < compose_node_id(b, 0).0);
396 }
397
398 #[test]
399 fn descend_into_pod_composes_stable_ids_and_restores_scope() {
400 let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), BASE_SEED);
401 let base = ctx.alloc_base();
402 assert_eq!(base.get(), BASE_SEED);
403 let (first, second) = {
405 let mut ids = (NodeId(0), NodeId(0));
406 ctx.descend_into_pod(base, Vec2::ZERO, Size::new(10.0, 10.0), |ctx| {
407 ids.0 = ctx.push_node(Role::Label, |_| {});
408 ids.1 = ctx.push_node(Role::Label, |_| {});
409 });
410 ids
411 };
412 assert_eq!(first, compose_node_id(base, 0));
413 assert_eq!(second, compose_node_id(base, 1));
414 let outside = ctx.push_node(Role::Label, |_| {});
418 assert_eq!(outside, NodeId(0));
419 }
420}