use std::num::NonZeroU64;
use accesskit::{Node, NodeId, Rect as AccessRect, Role};
use kurbo::{Point, Size, Vec2};
pub const ROOT_NODE_ID: NodeId = NodeId(1);
const SLOT_BITS: u32 = 16;
pub(crate) fn compose_node_id(base: NonZeroU64, slot: u16) -> NodeId {
debug_assert!(
base.get() < (1u64 << (64 - SLOT_BITS)),
"semantics base id {} overflows the {}-bit base field",
base.get(),
64 - SLOT_BITS
);
NodeId((base.get() << SLOT_BITS) | slot as u64)
}
#[derive(Clone, Debug, PartialEq)]
pub struct SemanticsUpdate {
pub nodes: Vec<(NodeId, Node)>,
pub root: NodeId,
pub focus: Option<NodeId>,
}
impl SemanticsUpdate {
pub fn focus_id(&self) -> NodeId {
self.focus.unwrap_or(self.root)
}
}
pub struct SemanticsCtx {
origin: Point,
size: Size,
next_id: u64,
next_base: u64,
current_base: Option<NonZeroU64>,
current_slot: u16,
nodes: Vec<(NodeId, Node)>,
frames: Vec<Vec<NodeId>>,
focus: Option<NodeId>,
}
impl SemanticsCtx {
pub(crate) fn new(window_size: Size, next_base: u64) -> Self {
Self {
origin: Point::ZERO,
size: window_size,
next_id: 0,
next_base,
current_base: None,
current_slot: 0,
nodes: Vec::new(),
frames: vec![Vec::new()],
focus: None,
}
}
pub fn origin(&self) -> Point {
self.origin
}
pub fn size(&self) -> Size {
self.size
}
pub(crate) fn alloc_base(&mut self) -> NonZeroU64 {
let id = self.next_base;
self.next_base += 1;
NonZeroU64::new(id).expect("base allocator is seeded >= 2, never zero")
}
pub(crate) fn next_base(&self) -> u64 {
self.next_base
}
fn alloc_id(&mut self) -> NodeId {
let id = NodeId(self.next_id);
self.next_id += 1;
id
}
fn next_node_id(&mut self) -> NodeId {
match self.current_base {
Some(base) => {
let slot = self.current_slot;
self.current_slot = self
.current_slot
.checked_add(1)
.expect("a single widget contributes fewer than 2^16 nodes per pod");
compose_node_id(base, slot)
}
None => self.alloc_id(),
}
}
fn bounds(&self) -> AccessRect {
AccessRect {
x0: self.origin.x,
y0: self.origin.y,
x1: self.origin.x + self.size.width,
y1: self.origin.y + self.size.height,
}
}
pub fn push_node(&mut self, role: Role, build: impl FnOnce(&mut Node)) -> NodeId {
let id = self.next_node_id();
let mut node = Node::new(role);
node.set_bounds(self.bounds());
build(&mut node);
self.attach(id, node);
id
}
pub fn push_container(
&mut self,
role: Role,
build: impl FnOnce(&mut Node),
visit: impl FnOnce(&mut SemanticsCtx),
) -> NodeId {
let id = self.next_node_id();
self.push_container_inner(id, role, build, visit)
}
pub(crate) fn push_container_with_id(
&mut self,
id: NodeId,
role: Role,
build: impl FnOnce(&mut Node),
visit: impl FnOnce(&mut SemanticsCtx),
) -> NodeId {
self.push_container_inner(id, role, build, visit)
}
fn push_container_inner(
&mut self,
id: NodeId,
role: Role,
build: impl FnOnce(&mut Node),
visit: impl FnOnce(&mut SemanticsCtx),
) -> NodeId {
let mut node = Node::new(role);
node.set_bounds(self.bounds());
build(&mut node);
self.frames.push(Vec::new());
visit(self);
let children = self.frames.pop().expect("container frame was just pushed");
node.set_children(children);
self.attach(id, node);
id
}
fn attach(&mut self, id: NodeId, node: Node) {
self.frames
.last_mut()
.expect("a child frame is always open during a pass")
.push(id);
self.nodes.push((id, node));
}
pub fn set_focused(&mut self, id: NodeId) {
self.focus = Some(id);
}
#[cfg(test)]
pub(crate) fn descend(
&mut self,
child_offset: Vec2,
child_size: Size,
f: impl FnOnce(&mut Self),
) {
let saved_origin = self.origin;
let saved_size = self.size;
self.origin = saved_origin + child_offset;
self.size = child_size;
f(self);
self.origin = saved_origin;
self.size = saved_size;
}
pub(crate) fn descend_into_pod(
&mut self,
base: NonZeroU64,
child_offset: Vec2,
child_size: Size,
f: impl FnOnce(&mut Self),
) {
let saved_origin = self.origin;
let saved_size = self.size;
let saved_base = self.current_base;
let saved_slot = self.current_slot;
self.origin = saved_origin + child_offset;
self.size = child_size;
self.current_base = Some(base);
self.current_slot = 0;
f(self);
self.origin = saved_origin;
self.size = saved_size;
self.current_base = saved_base;
self.current_slot = saved_slot;
}
pub(crate) fn finish(self, root: NodeId) -> SemanticsUpdate {
SemanticsUpdate {
nodes: self.nodes,
root,
focus: self.focus,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const BASE_SEED: u64 = 2;
#[test]
fn alloc_ids_are_sequential_and_deterministic() {
let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), BASE_SEED);
assert_eq!(ctx.alloc_id(), NodeId(0));
assert_eq!(ctx.alloc_id(), NodeId(1));
assert_eq!(ctx.alloc_id(), NodeId(2));
}
#[test]
fn push_node_sets_absolute_bounds_from_origin_and_size() {
let mut ctx = SemanticsCtx::new(Size::new(200.0, 200.0), BASE_SEED);
ctx.descend(Vec2::new(10.0, 20.0), Size::new(30.0, 40.0), |ctx| {
let id = ctx.push_node(Role::Label, |node| node.set_label("hi"));
assert_eq!(id, NodeId(0));
});
let (_, node) = &ctx.nodes[0];
assert_eq!(
node.bounds(),
Some(AccessRect {
x0: 10.0,
y0: 20.0,
x1: 40.0,
y1: 60.0,
})
);
assert_eq!(node.label(), Some("hi"));
assert_eq!(node.role(), Role::Label);
}
#[test]
fn descend_composes_nested_origins() {
let mut ctx = SemanticsCtx::new(Size::new(500.0, 500.0), BASE_SEED);
ctx.descend(Vec2::new(100.0, 200.0), Size::new(300.0, 300.0), |ctx| {
ctx.descend(Vec2::new(5.0, 7.0), Size::new(10.0, 10.0), |ctx| {
ctx.push_node(Role::Button, |_| {});
});
});
let (_, node) = &ctx.nodes[0];
assert_eq!(
node.bounds(),
Some(AccessRect {
x0: 105.0,
y0: 207.0,
x1: 115.0,
y1: 217.0,
})
);
}
#[test]
fn push_container_collects_children_and_restores_frame() {
let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), BASE_SEED);
let container = ctx.push_container(
Role::ScrollView,
|_| {},
|ctx| {
ctx.push_node(Role::Label, |n| n.set_label("a"));
ctx.push_node(Role::Label, |n| n.set_label("b"));
},
);
assert_eq!(container, NodeId(0));
let update = ctx.finish(container);
assert_eq!(update.nodes.len(), 3);
let container_node = &update
.nodes
.iter()
.find(|(id, _)| *id == container)
.unwrap()
.1;
assert_eq!(container_node.children(), &[NodeId(1), NodeId(2)]);
}
#[test]
fn set_focused_records_the_focus_node() {
let mut ctx = SemanticsCtx::new(Size::new(10.0, 10.0), BASE_SEED);
let id = ctx.push_node(Role::TextInput, |_| {});
ctx.set_focused(id);
let update = ctx.finish(id);
assert_eq!(update.focus, Some(id));
}
#[test]
fn focus_id_defaults_to_root_when_unfocused() {
let mut ctx = SemanticsCtx::new(Size::new(10.0, 10.0), BASE_SEED);
let root = ctx.push_container(Role::Window, |_| {}, |_| {});
let update = ctx.finish(root);
assert!(update.focus.is_none());
assert_eq!(update.focus_id(), update.root);
}
#[test]
fn compose_node_id_is_collision_free_across_bases_and_slots() {
use std::collections::HashSet;
let mut seen = HashSet::new();
for base in 2u64..40 {
let base = NonZeroU64::new(base).unwrap();
for slot in 0u16..1000 {
let id = compose_node_id(base, slot);
assert!(id != ROOT_NODE_ID, "never collides with the reserved root");
assert!(seen.insert(id), "duplicate id for base={base} slot={slot}");
}
}
let a = NonZeroU64::new(2).unwrap();
let b = NonZeroU64::new(3).unwrap();
assert!(compose_node_id(a, u16::MAX).0 < compose_node_id(b, 0).0);
}
#[test]
fn descend_into_pod_composes_stable_ids_and_restores_scope() {
let mut ctx = SemanticsCtx::new(Size::new(100.0, 100.0), BASE_SEED);
let base = ctx.alloc_base();
assert_eq!(base.get(), BASE_SEED);
let (first, second) = {
let mut ids = (NodeId(0), NodeId(0));
ctx.descend_into_pod(base, Vec2::ZERO, Size::new(10.0, 10.0), |ctx| {
ids.0 = ctx.push_node(Role::Label, |_| {});
ids.1 = ctx.push_node(Role::Label, |_| {});
});
ids
};
assert_eq!(first, compose_node_id(base, 0));
assert_eq!(second, compose_node_id(base, 1));
let outside = ctx.push_node(Role::Label, |_| {});
assert_eq!(outside, NodeId(0));
}
}