use std::collections::VecDeque;
use accesskit::{Live, NodeId, Role};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Politeness {
#[default]
Polite,
Assertive,
}
impl Politeness {
fn live(self) -> Live {
match self {
Self::Polite => Live::Polite,
Self::Assertive => Live::Assertive,
}
}
fn role(self) -> Role {
match self {
Self::Polite => Role::Status,
Self::Assertive => Role::Alert,
}
}
fn node_id(self) -> NodeId {
match self {
Self::Polite => NodeId(1),
Self::Assertive => NodeId(2),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Phase {
Idle,
Speaking(String),
Retracting,
}
#[derive(Debug)]
pub(crate) struct Announcer {
politeness: Politeness,
phase: Phase,
pending: VecDeque<String>,
}
impl Announcer {
pub(crate) fn new(politeness: Politeness) -> Self {
Self {
politeness,
phase: Phase::Idle,
pending: VecDeque::new(),
}
}
pub(crate) fn push(&mut self, message: String) {
if message.trim().is_empty() {
return;
}
if self.pending.len() >= MAX_QUEUED {
self.pending.pop_front();
}
self.pending.push_back(message);
}
pub(crate) fn step(&mut self) -> bool {
self.phase = match std::mem::replace(&mut self.phase, Phase::Idle) {
Phase::Speaking(_) => Phase::Retracting,
_ => match self.pending.pop_front() {
Some(message) => Phase::Speaking(message),
None => Phase::Idle,
},
};
matches!(self.phase, Phase::Speaking(_)) || !self.pending.is_empty()
}
pub(crate) fn node(&self) -> (NodeId, accesskit::Node) {
let mut node = accesskit::Node::new(self.politeness.role());
node.set_live(self.politeness.live());
match &self.phase {
Phase::Speaking(message) => {
node.set_label(message.clone());
}
Phase::Idle | Phase::Retracting => {
node.set_hidden();
}
}
(self.politeness.node_id(), node)
}
}
const MAX_QUEUED: usize = 32;
#[cfg(test)]
mod tests {
use super::*;
fn label_of(a: &Announcer) -> Option<String> {
let (_, node) = a.node();
node.label().map(|s| s.to_string())
}
fn hidden(a: &Announcer) -> bool {
let (_, node) = a.node();
node.is_hidden()
}
fn node_id(a: &Announcer) -> NodeId {
a.node().0
}
#[test]
fn an_idle_announcer_is_hidden_and_unlabelled() {
let mut a = Announcer::new(Politeness::Polite);
assert!(hidden(&a));
assert_eq!(label_of(&a), None);
assert!(!a.step(), "an idle announcer has nothing to do");
}
#[test]
fn a_message_is_exposed_then_retracted() {
let mut a = Announcer::new(Politeness::Polite);
a.push("Event added".to_string());
assert!(a.step(), "still busy: the retract is yet to come");
assert!(!hidden(&a), "the node must enter the filtered tree");
assert_eq!(label_of(&a).as_deref(), Some("Event added"));
assert!(!a.step(), "nothing left after the retract");
assert!(hidden(&a), "the node must leave the filtered tree again");
}
#[test]
fn the_same_message_twice_is_exposed_twice() {
let mut a = Announcer::new(Politeness::Polite);
a.push("Saved".to_string());
a.push("Saved".to_string());
a.step();
assert!(!hidden(&a));
assert_eq!(label_of(&a).as_deref(), Some("Saved"));
a.step();
assert!(hidden(&a), "the node must be retracted between the two");
a.step();
assert!(!hidden(&a));
assert_eq!(label_of(&a).as_deref(), Some("Saved"));
assert!(!a.step());
assert!(hidden(&a));
}
#[test]
fn messages_queue_rather_than_replace_one_another() {
let mut a = Announcer::new(Politeness::Polite);
a.push("first".to_string());
a.push("second".to_string());
let mut spoken = Vec::new();
for _ in 0..4 {
a.step();
if let Some(l) = label_of(&a) {
spoken.push(l);
}
}
assert_eq!(spoken, vec!["first", "second"]);
}
#[test]
fn an_empty_message_is_dropped_rather_than_costing_a_cycle() {
let mut a = Announcer::new(Politeness::Polite);
a.push(String::new());
a.push(" \n\t ".to_string());
assert!(!a.step());
}
#[test]
fn a_runaway_queue_drops_the_oldest_not_the_newest() {
let mut a = Announcer::new(Politeness::Polite);
for i in 0..(MAX_QUEUED + 5) {
a.push(format!("message {i}"));
}
a.step();
assert_eq!(
label_of(&a).as_deref(),
Some("message 5"),
"the five oldest must be the ones dropped"
);
}
#[test]
fn the_two_politeness_levels_use_distinct_reserved_nodes() {
let polite = node_id(&Announcer::new(Politeness::Polite));
let assertive = node_id(&Announcer::new(Politeness::Assertive));
assert_ne!(polite, assertive);
assert_ne!(polite, crate::accessibility::root_node_id());
assert_ne!(assertive, crate::accessibility::root_node_id());
assert!(!crate::accessibility::is_synthetic(polite));
assert!(!crate::accessibility::is_synthetic(assertive));
}
#[test]
fn politeness_maps_to_the_aria_role_and_live_setting() {
let (_, polite) = Announcer::new(Politeness::Polite).node();
assert_eq!(polite.role(), Role::Status);
assert_eq!(polite.live(), Some(Live::Polite));
let (_, assertive) = Announcer::new(Politeness::Assertive).node();
assert_eq!(assertive.role(), Role::Alert);
assert_eq!(assertive.live(), Some(Live::Assertive));
}
}