1use std::cell::RefCell;
2
3use cranpose_core::CompositionLocal;
4use cranpose_foundation::LiveRegionMode;
5
6const QUEUE_LIMIT: usize = 32;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct Announcement {
11 pub text: String,
12 pub mode: LiveRegionMode,
13}
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct Announcer;
30
31impl Announcer {
32 pub fn announce(&self, text: impl Into<String>) {
34 push(text.into(), LiveRegionMode::Polite);
35 }
36
37 pub fn announce_assertive(&self, text: impl Into<String>) {
39 push(text.into(), LiveRegionMode::Assertive);
40 }
41}
42
43pub fn local_announcer() -> CompositionLocal<Announcer> {
46 crate::environment_locals::cached_local(
47 |locals| &locals.announcer,
48 || cranpose_core::compositionLocalOf(Announcer::default),
49 )
50}
51
52pub fn announce(text: impl Into<String>) {
55 Announcer.announce(text);
56}
57
58pub fn drain_announcements() -> Vec<Announcement> {
61 queue(std::mem::take)
62}
63
64pub fn pending_announcements() -> usize {
66 queue(|pending| pending.len())
67}
68
69fn push(text: String, mode: LiveRegionMode) {
70 if text.trim().is_empty() {
71 return;
72 }
73 queue(|pending| {
74 if pending.len() >= QUEUE_LIMIT {
75 pending.remove(0);
76 }
77 pending.push(Announcement { text, mode });
78 });
79}
80
81fn queue<T>(action: impl FnOnce(&mut Vec<Announcement>) -> T) -> T {
82 thread_local! {
83 static PENDING: RefCell<Vec<Announcement>> = const { RefCell::new(Vec::new()) };
84 }
85
86 PENDING.with(|cell| action(&mut cell.borrow_mut()))
87}
88
89#[cfg(test)]
90#[path = "tests/announce_tests.rs"]
91mod tests;