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 thread_local! {
47 static LOCAL_ANNOUNCER: RefCell<Option<CompositionLocal<Announcer>>> =
48 const { RefCell::new(None) };
49 }
50
51 LOCAL_ANNOUNCER.with(|cell| {
52 cell.borrow_mut()
53 .get_or_insert_with(|| cranpose_core::compositionLocalOf(Announcer::default))
54 .clone()
55 })
56}
57
58pub fn announce(text: impl Into<String>) {
61 Announcer.announce(text);
62}
63
64pub fn drain_announcements() -> Vec<Announcement> {
67 queue(std::mem::take)
68}
69
70pub fn pending_announcements() -> usize {
72 queue(|pending| pending.len())
73}
74
75fn push(text: String, mode: LiveRegionMode) {
76 if text.trim().is_empty() {
77 return;
78 }
79 queue(|pending| {
80 if pending.len() >= QUEUE_LIMIT {
81 pending.remove(0);
82 }
83 pending.push(Announcement { text, mode });
84 });
85}
86
87fn queue<T>(action: impl FnOnce(&mut Vec<Announcement>) -> T) -> T {
88 thread_local! {
89 static PENDING: RefCell<Vec<Announcement>> = const { RefCell::new(Vec::new()) };
90 }
91
92 PENDING.with(|cell| action(&mut cell.borrow_mut()))
93}
94
95#[cfg(test)]
96#[path = "tests/announce_tests.rs"]
97mod tests;