use std::collections::VecDeque;
use std::time::{Duration, Instant};
use accesskit::{Live, NodeId, Role};
pub const LINGER: Duration = Duration::from_secs(5);
const MAX_SHOWN: usize = 8;
#[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,
}
}
}
const FIRST_ID: u64 = 1;
const END_ID: u64 = 1 << 32;
#[derive(Debug)]
pub(crate) struct AnnouncerIds {
next: u64,
}
impl AnnouncerIds {
pub(crate) fn new() -> Self {
Self { next: FIRST_ID }
}
fn take(&mut self) -> NodeId {
let id = NodeId(self.next);
self.next = if self.next + 1 >= END_ID {
FIRST_ID
} else {
self.next + 1
};
id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Shown {
id: NodeId,
message: String,
until: Instant,
}
#[derive(Debug)]
pub(crate) struct Announcer {
politeness: Politeness,
pending: VecDeque<String>,
entering: Option<(NodeId, String)>,
shown: VecDeque<Shown>,
}
impl Announcer {
pub(crate) fn new(politeness: Politeness) -> Self {
Self {
politeness,
pending: VecDeque::new(),
entering: None,
shown: 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, now: Instant, ids: &mut AnnouncerIds) -> bool {
let before = self.shown.len();
self.shown.retain(|shown| shown.until > now);
if self.entering.is_none()
&& let Some(message) = self.pending.pop_front()
{
self.entering = Some((ids.take(), message));
}
self.shown.len() != before || self.entering.is_some()
}
pub(crate) fn entering(&self) -> Option<&str> {
self.entering.as_ref().map(|(_, message)| message.as_str())
}
pub(crate) fn admit(&mut self, now: Instant) {
let Some((id, message)) = self.entering.take() else {
return;
};
if self.shown.len() >= MAX_SHOWN {
self.shown.pop_front();
}
self.shown.push_back(Shown {
id,
message,
until: now + LINGER,
});
}
pub(crate) fn busy(&self) -> bool {
self.entering.is_some() || !self.pending.is_empty()
}
pub(crate) fn next_retirement(&self) -> Option<Instant> {
self.shown.iter().map(|shown| shown.until).min()
}
pub(crate) fn rebase(&mut self, from: Instant, to: Instant) {
for shown in &mut self.shown {
shown.until = if to >= from {
shown.until + (to - from)
} else {
shown.until.checked_sub(from - to).unwrap_or(to)
};
}
}
pub(crate) fn nodes(
&self,
admit: bool,
) -> impl Iterator<Item = (NodeId, accesskit::Node)> + '_ {
let entering = self.entering.as_ref().filter(|_| admit);
let full = entering.is_some() && self.shown.len() >= MAX_SHOWN;
self.shown
.iter()
.skip(usize::from(full))
.map(|shown| (shown.id, shown.message.as_str()))
.chain(entering.map(|(id, message)| (*id, message.as_str())))
.map(|(id, message)| {
let mut node = accesskit::Node::new(self.politeness.role());
node.set_live(self.politeness.live());
node.set_label(message);
(id, node)
})
}
}
pub(crate) fn is_announcer_node(id: NodeId) -> bool {
(FIRST_ID..END_ID).contains(&id.0)
}
const MAX_QUEUED: usize = 32;
#[cfg(test)]
mod tests {
use super::*;
fn at(start: Instant, millis: u64) -> Instant {
start + Duration::from_millis(millis)
}
fn labels(a: &Announcer) -> Vec<String> {
a.nodes(false)
.map(|(_, node)| node.label().unwrap_or_default().to_string())
.collect()
}
fn ids(a: &Announcer) -> Vec<NodeId> {
a.nodes(false).map(|(id, _)| id).collect()
}
fn update(a: &mut Announcer, now: Instant, ids: &mut AnnouncerIds) -> bool {
let changed = a.step(now, ids);
a.admit(now);
changed
}
#[test]
fn an_idle_announcer_puts_nothing_in_the_tree() {
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
assert!(!update(&mut a, Instant::now(), &mut ids_));
assert_eq!(a.next_retirement(), None);
assert_eq!(a.nodes(true).count(), 0);
}
#[test]
fn a_message_stays_for_its_linger_and_then_leaves() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
a.push("Event added".to_string());
assert!(update(&mut a, start, &mut ids_) && !a.busy());
assert_eq!(a.next_retirement(), Some(start + LINGER));
assert_eq!(labels(&a), ["Event added"]);
assert!(
!update(&mut a, start + LINGER - Duration::from_millis(1), &mut ids_),
"nothing changes while the message lingers"
);
assert_eq!(labels(&a), ["Event added"]);
assert!(update(&mut a, start + LINGER, &mut ids_));
assert_eq!(a.next_retirement(), None);
assert_eq!(a.nodes(true).count(), 0);
}
#[test]
fn the_same_message_twice_is_two_new_nodes() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
a.push("Saved".to_string());
update(&mut a, start, &mut ids_);
let first = ids(&a);
update(&mut a, start + LINGER, &mut ids_);
a.push("Saved".to_string());
update(&mut a, at(start + LINGER, 1), &mut ids_);
let second = ids(&a);
assert_eq!(labels(&a), ["Saved"]);
assert_eq!(first.len(), 1);
assert_eq!(second.len(), 1);
assert_ne!(
first, second,
"a message must never be spoken from an id used before"
);
}
#[test]
fn messages_queue_one_an_update_and_keep_their_order() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
a.push("first".to_string());
a.push("second".to_string());
update(&mut a, start, &mut ids_);
assert!(a.busy(), "the second message is owed an update of its own");
assert_eq!(labels(&a), ["first"]);
update(&mut a, at(start, 16), &mut ids_);
assert!(!a.busy());
assert_eq!(labels(&a), ["first", "second"]);
assert_eq!(
a.next_retirement(),
Some(start + LINGER),
"the earliest leaves first"
);
}
#[test]
fn a_waiting_message_keeps_its_id_and_its_place() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
a.push("first".to_string());
a.push("second".to_string());
assert!(a.step(start, &mut ids_));
let ready: Vec<NodeId> = a.nodes(true).map(|(id, _)| id).collect();
assert_eq!(a.nodes(false).count(), 0, "held, so nothing in the tree");
assert!(
a.step(at(start, 16), &mut ids_),
"a waiting message owes the next update a walk"
);
assert_eq!(a.entering(), Some("first"), "the second does not jump it");
assert_eq!(
a.nodes(true).map(|(id, _)| id).collect::<Vec<_>>(),
ready,
"the id it was given is the one it enters with"
);
a.admit(at(start, 16));
assert_eq!(labels(&a), ["first"]);
assert_eq!(a.next_retirement(), Some(at(start, 16) + LINGER));
assert!(a.busy(), "the second is still owed an update");
update(&mut a, at(start, 32), &mut ids_);
assert_eq!(labels(&a), ["first", "second"]);
assert!(!a.busy());
}
#[test]
fn an_empty_message_is_dropped() {
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
a.push(String::new());
a.push(" \n\t ".to_string());
assert!(!update(&mut a, Instant::now(), &mut ids_));
}
#[test]
fn a_runaway_queue_drops_the_oldest_not_the_newest() {
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
for i in 0..(MAX_QUEUED + 5) {
a.push(format!("message {i}"));
}
update(&mut a, Instant::now(), &mut ids_);
assert_eq!(
labels(&a),
["message 5"],
"the five oldest must be the ones dropped"
);
}
#[test]
fn a_burst_keeps_at_most_max_shown_in_the_tree() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
for i in 0..20 {
a.push(format!("row {i}"));
}
for frame in 0..20 {
a.step(at(start, frame * 16), &mut ids_);
assert!(
a.nodes(true).count() <= MAX_SHOWN,
"the update that admits a message into a full tree carries no more"
);
a.admit(at(start, frame * 16));
assert!(a.nodes(false).count() <= MAX_SHOWN);
}
let last: Vec<String> = (12..20).map(|i| format!("row {i}")).collect();
assert_eq!(labels(&a), last);
}
#[test]
fn announcer_ids_are_unique_and_outside_every_other_range() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut polite = Announcer::new(Politeness::Polite);
let mut assertive = Announcer::new(Politeness::Assertive);
let mut seen = std::collections::HashSet::new();
for i in 0..20 {
polite.push(format!("p{i}"));
assertive.push(format!("a{i}"));
update(&mut polite, at(start, i * 16), &mut ids_);
update(&mut assertive, at(start, i * 16), &mut ids_);
for id in ids(&polite).into_iter().chain(ids(&assertive)) {
seen.insert(id);
assert!(is_announcer_node(id));
assert_ne!(id, crate::accessibility::root_node_id());
assert!(!crate::accessibility::is_synthetic(id));
assert!(id.0 < 1 << 32, "a widget id is at least 1 << 32");
}
}
assert_eq!(seen.len(), 40, "forty messages, forty ids");
}
#[test]
fn the_counter_wraps_inside_its_range() {
let mut ids_ = AnnouncerIds { next: END_ID - 1 };
assert_eq!(ids_.take(), NodeId(END_ID - 1));
assert_eq!(ids_.take(), NodeId(FIRST_ID));
}
#[test]
fn a_rebase_keeps_the_time_a_message_has_left() {
let start = Instant::now();
let mut ids_ = AnnouncerIds::new();
let mut a = Announcer::new(Politeness::Polite);
a.push("Saved".to_string());
update(&mut a, start, &mut ids_);
let later = start + Duration::from_secs(100);
a.rebase(start, later);
assert!(!update(
&mut a,
later + LINGER - Duration::from_millis(1),
&mut ids_
));
assert_eq!(labels(&a), ["Saved"]);
update(&mut a, later + LINGER, &mut ids_);
assert_eq!(a.nodes(true).count(), 0);
}
#[test]
fn politeness_maps_to_the_aria_role_and_live_setting() {
let mut ids_ = AnnouncerIds::new();
let mut polite = Announcer::new(Politeness::Polite);
polite.push("p".to_string());
update(&mut polite, Instant::now(), &mut ids_);
let (_, node) = polite.nodes(false).next().unwrap();
assert_eq!(node.role(), Role::Status);
assert_eq!(node.live(), Some(Live::Polite));
let mut assertive = Announcer::new(Politeness::Assertive);
assertive.push("a".to_string());
update(&mut assertive, Instant::now(), &mut ids_);
let (_, node) = assertive.nodes(false).next().unwrap();
assert_eq!(node.role(), Role::Alert);
assert_eq!(node.live(), Some(Live::Assertive));
}
}