use std::collections::VecDeque;
use accesskit::{Live, NodeId, TreeUpdate};
use accesskit_consumer::{FilterResult, NodeRef, Tree, TreeChangeHandler, common_filter};
use super::Announcement;
pub(crate) const CAPACITY: usize = 256;
pub(crate) struct AnnouncementRing {
recording: bool,
replay: Option<Tree>,
reseed_quietly: bool,
entries: VecDeque<Announcement>,
last_seq: u64,
}
impl AnnouncementRing {
pub(crate) fn new() -> Self {
Self {
recording: true,
replay: None,
reseed_quietly: false,
entries: VecDeque::new(),
last_seq: 0,
}
}
pub(crate) fn observe(&mut self, update: &TreeUpdate) {
if !self.recording {
self.replay = None;
self.reseed_quietly = true;
return;
}
let replay = self.replay.take();
if replay.is_none() && update.tree.is_none() {
return;
}
let quietly = self.reseed_quietly;
let applied = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
let mut heard = Heard::default();
let tree = match replay {
Some(mut tree) => {
tree.update_and_process_changes(update.clone(), &mut heard);
tree
}
None if quietly => Tree::new(update.clone(), true),
None => {
let mut tree = Tree::new(before_first_draw(), true);
tree.update_and_process_changes(update.clone(), &mut heard);
tree
}
};
let spoken = heard.in_reading_order(&tree);
(tree, spoken)
}));
match applied {
Ok((tree, spoken)) => {
self.replay = Some(tree);
self.reseed_quietly = false;
for (text, assertive) in spoken {
self.record(text, assertive);
}
}
Err(_) => self.reseed_quietly = true,
}
}
fn record(&mut self, text: String, assertive: bool) {
self.last_seq = self.last_seq.saturating_add(1);
if self.entries.len() >= CAPACITY {
self.entries.pop_front();
}
self.entries.push_back(Announcement {
seq: self.last_seq,
text,
assertive,
});
}
pub(crate) fn is_recording(&self) -> bool {
self.recording
}
pub(crate) fn set_recording(&mut self, recording: bool) {
self.recording = recording;
}
#[cfg(test)]
pub(crate) fn holds_a_replay(&self) -> bool {
self.replay.is_some()
}
pub(crate) fn since(&self, seq: u64) -> Vec<Announcement> {
self.entries
.iter()
.filter(|announcement| announcement.seq > seq)
.cloned()
.collect()
}
}
fn before_first_draw() -> TreeUpdate {
let root = super::root_node_id();
TreeUpdate {
nodes: vec![(root, accesskit::Node::new(accesskit::Role::Window))],
tree: Some(accesskit::TreeInfo::new(root)),
tree_id: accesskit::TreeId::ROOT,
focus: root,
}
}
pub(crate) fn speaks_within(update: &TreeUpdate, owned: impl Fn(NodeId) -> bool) -> bool {
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Tree::new(update.clone(), true)
}));
let Ok(tree) = built else {
return false;
};
let state = tree.state();
let mut stack = vec![state.root()];
while let Some(node) = stack.pop() {
if owned(node.locate().0)
&& common_filter(&node) == FilterResult::Include
&& node.live() != Live::Off
&& politeness_source(node).is_some_and(|source| owned(source.locate().0))
&& spoken_name(&node).is_some_and(|name| !name.trim().is_empty())
{
return true;
}
stack.extend(node.children());
}
false
}
fn politeness_source(node: NodeRef<'_>) -> Option<NodeRef<'_>> {
let mut current = Some(node);
while let Some(candidate) = current {
if candidate.data().live().is_some() {
return Some(candidate);
}
current = candidate.parent();
}
None
}
fn spoken_name(node: &NodeRef<'_>) -> Option<String> {
if node.label_comes_from_value() {
node.value()
} else {
node.label()
}
}
#[derive(Default)]
struct Heard {
spoken: Vec<(NodeId, String, bool)>,
}
impl Heard {
fn hear(&mut self, node: &NodeRef<'_>) {
let live = node.live();
if live == Live::Off {
return;
}
let Some(name) = spoken_name(node) else {
return;
};
if name.trim().is_empty() {
return;
}
self.spoken
.push((node.locate().0, name, live == Live::Assertive));
}
fn in_reading_order(mut self, replay: &Tree) -> Vec<(String, bool)> {
if self.spoken.len() > 1 {
let mut position: std::collections::HashMap<NodeId, usize> =
std::collections::HashMap::new();
let mut stack = vec![replay.state().root()];
while let Some(node) = stack.pop() {
position.insert(node.locate().0, position.len());
stack.extend(node.children().rev());
}
self.spoken
.sort_by_key(|(id, _, _)| position.get(id).copied().unwrap_or(usize::MAX));
}
self.spoken
.into_iter()
.map(|(_, text, assertive)| (text, assertive))
.collect()
}
}
impl TreeChangeHandler for Heard {
fn node_added(&mut self, node: &NodeRef<'_>) {
if common_filter(node) == FilterResult::Include {
self.hear(node);
}
}
fn node_updated(&mut self, old: &NodeRef<'_>, new: &NodeRef<'_>) {
if common_filter(new) != FilterResult::Include {
return;
}
let entered = common_filter(old) != FilterResult::Include;
if entered || spoken_name(old) != spoken_name(new) {
self.hear(new);
}
}
fn focus_moved(&mut self, _old: Option<&NodeRef<'_>>, _new: Option<&NodeRef<'_>>) {}
fn node_removed(&mut self, _node: &NodeRef<'_>) {}
}
#[cfg(test)]
mod tests {
use accesskit::{Live, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
use super::AnnouncementRing;
const ROOT: NodeId = NodeId(0);
const STATUS: NodeId = NodeId(10);
fn window_with_status(name: &str) -> TreeUpdate {
let mut root = Node::new(Role::Window);
root.set_children(vec![STATUS]);
let mut status = Node::new(Role::Status);
status.set_label(name.to_string());
status.set_live(Live::Polite);
TreeUpdate {
nodes: vec![(ROOT, root), (STATUS, status)],
tree: Some(TreeInfo::new(ROOT)),
tree_id: TreeId::ROOT,
focus: ROOT,
}
}
fn heard(ring: &AnnouncementRing, since: u64) -> Vec<String> {
ring.since(since).into_iter().map(|a| a.text).collect()
}
#[test]
fn an_update_the_consumer_rejects_is_survived() {
let mut ring = AnnouncementRing::new();
ring.observe(&window_with_status("Prêt"));
assert_eq!(heard(&ring, 0), vec!["Prêt"]);
let seen = 1;
let mut broken = window_with_status("Prêt");
broken.nodes[0].1.set_children(vec![STATUS, NodeId(99)]);
ring.observe(&broken);
assert_eq!(heard(&ring, seen), Vec::<String>::new());
ring.observe(&window_with_status("Prêt"));
assert_eq!(
heard(&ring, seen),
Vec::<String>::new(),
"reseeding is not an arrival"
);
ring.observe(&window_with_status("Enregistré"));
assert_eq!(heard(&ring, seen), vec!["Enregistré"]);
}
#[test]
fn the_ring_keeps_the_most_recent() {
let mut ring = AnnouncementRing::new();
for index in 0..(super::CAPACITY + 10) {
ring.observe(&window_with_status(&format!("message {index}")));
}
let kept = ring.since(0);
assert_eq!(kept.len(), super::CAPACITY);
assert_eq!(
kept.last().map(|a| a.text.as_str()),
Some(format!("message {}", super::CAPACITY + 9).as_str())
);
assert_eq!(
kept.first().map(|a| a.seq),
Some(11),
"sequence numbers keep counting past what was dropped"
);
}
}