ravenrs_app_a11y/
announce.rs1use std::cell::Cell;
9use std::rc::Rc;
10
11use ravenrs_core::Role;
12use ravenrs_layout::LayoutStyle;
13use ravenrs_reactive::State;
14use ravenrs_widgets::{BuildCx, Keyed, NodeId, Paint, Semantics, View};
15
16const KEPT: usize = 3;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub enum Urgency {
22 #[default]
24 Polite,
25 Assertive,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct Announcement {
33 pub text: String,
35 pub urgency: Urgency,
37 pub serial: u64,
39}
40
41#[derive(Clone, Debug, Default)]
59pub struct Announcer {
60 recent: State<Vec<Announcement>>,
61 count: Rc<Cell<u64>>,
62 quiet: Rc<Cell<bool>>,
63}
64
65impl Announcer {
66 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn polite(&self, text: impl Into<String>) {
73 self.say(text.into(), Urgency::Polite);
74 }
75
76 pub fn assertive(&self, text: impl Into<String>) {
78 self.say(text.into(), Urgency::Assertive);
79 }
80
81 pub fn say(&self, text: String, urgency: Urgency) {
83 if self.quiet.get() || text.trim().is_empty() {
84 return;
85 }
86 let serial = self.count.get() + 1;
87 self.count.set(serial);
88 self.recent.update(|recent| {
89 recent.push(Announcement { text, urgency, serial });
90 let extra = recent.len().saturating_sub(KEPT);
91 recent.drain(..extra);
92 });
93 }
94
95 pub fn set_quiet(&self, quiet: bool) {
97 self.quiet.set(quiet);
98 }
99
100 pub fn is_quiet(&self) -> bool {
102 self.quiet.get()
103 }
104
105 pub fn latest(&self) -> Option<Announcement> {
107 self.recent.peek().last().cloned()
108 }
109
110 pub fn recent(&self) -> Vec<Announcement> {
112 self.recent.peek().clone()
113 }
114
115 pub fn region(&self) -> LiveRegion {
117 LiveRegion { recent: self.recent.clone() }
118 }
119}
120
121#[derive(Clone, Debug)]
123pub struct LiveRegion {
124 recent: State<Vec<Announcement>>,
125}
126
127struct Said(Announcement);
129
130impl View for Said {
131 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
132 let node = cx.container(
133 LayoutStyle { width: Some(0.0), height: Some(0.0), ..LayoutStyle::leaf() },
134 &[],
135 Paint::None,
136 );
137 let role = match self.0.urgency {
138 Urgency::Polite => Role::Status,
139 Urgency::Assertive => Role::Alert,
140 };
141 cx.semantics(node, Semantics::new(role).name(self.0.text.clone()));
142 node
143 }
144}
145
146impl View for LiveRegion {
147 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
148 let said: Vec<NodeId> = self
149 .recent
150 .get()
151 .into_iter()
152 .map(|said| cx.child(&Keyed::new(said.serial, Said(said))))
153 .collect();
154 cx.container(
155 LayoutStyle { width: Some(0.0), height: Some(0.0), ..LayoutStyle::leaf() },
156 &said,
157 Paint::None,
158 )
159 }
160}