1use super::{Status, UiContext, UiEvent, UiSound, Value, Widget};
4use crate::input::Key;
5
6#[derive(Debug, Clone)]
9pub struct Message {
10 id: String,
11 text: String,
12 lines: Vec<String>,
13 line: usize,
14}
15
16impl Message {
17 pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
19 let text = text.into();
20 let lines = text.lines().map(str::trim).filter(|l| !l.is_empty()).map(String::from).collect();
21 Self { id: id.into(), text, lines, line: 0 }
22 }
23}
24
25impl Widget for Message {
26 fn id(&self) -> &str {
27 &self.id
28 }
29
30 fn open(&mut self, ui: &mut UiContext<'_>) {
31 ui.play(UiSound::Open, None);
32 ui.speech.speak(&self.text);
33 }
34
35 fn update(&mut self, ui: &mut UiContext<'_>) -> Status {
36 let last = self.lines.len().saturating_sub(1);
37 let target = if ui.key(Key::Up) {
38 Some(self.line.saturating_sub(1))
39 } else if ui.key(Key::Down) {
40 Some((self.line + 1).min(last))
41 } else if ui.key(Key::Home) {
42 Some(0)
43 } else if ui.key(Key::End) {
44 Some(last)
45 } else {
46 None
47 };
48 if let Some(t) = target {
49 ui.play(if t == self.line { UiSound::Edge } else { UiSound::Navigate }, None);
50 self.line = t;
51 if let Some(line) = self.lines.get(t) {
52 ui.speech.speak(line);
53 }
54 }
55 if ui.key(Key::Enter) || ui.key(Key::NumpadEnter) || ui.input.pressed(Key::Space) || ui.key(Key::Escape) {
56 ui.play(UiSound::Close, None);
57 ui.emit(UiEvent::Closed { id: self.id.clone() });
58 return Status::Close;
59 }
60 Status::Open
61 }
62}
63
64#[derive(Debug, Clone)]
67pub struct Confirm {
68 id: String,
69 question: String,
70 yes: String,
71 no: String,
72 selected_yes: bool,
73}
74
75impl Confirm {
76 pub fn new(id: impl Into<String>, question: impl Into<String>) -> Self {
78 Self { id: id.into(), question: question.into(), yes: "Yes".into(), no: "No".into(), selected_yes: true }
79 }
80
81 pub fn labels(mut self, yes: impl Into<String>, no: impl Into<String>) -> Self {
83 self.yes = yes.into();
84 self.no = no.into();
85 self
86 }
87
88 pub fn default_no(mut self) -> Self {
90 self.selected_yes = false;
91 self
92 }
93
94 fn current(&self) -> &str {
95 if self.selected_yes { &self.yes } else { &self.no }
96 }
97
98 fn answer(&self, yes: bool, ui: &mut UiContext<'_>) -> Status {
99 ui.play(if yes { UiSound::Activate } else { UiSound::Close }, None);
100 ui.emit(UiEvent::Confirmed { id: self.id.clone(), yes });
101 Status::Close
102 }
103}
104
105fn first_lower(s: &str) -> Option<char> {
106 s.chars().next().and_then(|c| c.to_lowercase().next())
107}
108
109impl Widget for Confirm {
110 fn id(&self) -> &str {
111 &self.id
112 }
113
114 fn open(&mut self, ui: &mut UiContext<'_>) {
115 ui.play(UiSound::Open, None);
116 ui.speech.speak(format!("{} {}", self.question, self.current()));
117 }
118
119 fn update(&mut self, ui: &mut UiContext<'_>) -> Status {
120 if ui.key(Key::Up) || ui.key(Key::Down) || ui.key(Key::Left) || ui.key(Key::Right) || ui.key(Key::Tab) {
121 self.selected_yes = !self.selected_yes;
122 ui.play(UiSound::Navigate, None);
123 let text = self.current().to_string();
124 ui.speech.speak(text);
125 } else if ui.key(Key::Enter) || ui.key(Key::NumpadEnter) || ui.input.pressed(Key::Space) {
126 return self.answer(self.selected_yes, ui);
127 } else if ui.key(Key::Escape) {
128 return self.answer(false, ui);
129 } else if let Some(c) = ui.input.text().chars().next().and_then(|c| c.to_lowercase().next()) {
130 if Some(c) == first_lower(&self.yes) {
131 return self.answer(true, ui);
132 }
133 if Some(c) == first_lower(&self.no) {
134 return self.answer(false, ui);
135 }
136 }
137 Status::Open
138 }
139}
140
141#[derive(Debug, Clone)]
154pub struct TextInput {
155 id: String,
156 prompt: String,
157 text: Vec<char>,
158 cursor: usize,
159 password: bool,
160 max_length: Option<usize>,
161 numeric: bool,
162 allow_empty: bool,
163 child: bool,
164}
165
166impl TextInput {
167 pub fn new(id: impl Into<String>, prompt: impl Into<String>) -> Self {
169 Self {
170 id: id.into(),
171 prompt: prompt.into(),
172 text: Vec::new(),
173 cursor: 0,
174 password: false,
175 max_length: None,
176 numeric: false,
177 allow_empty: true,
178 child: false,
179 }
180 }
181
182 pub fn initial(mut self, text: impl Into<String>) -> Self {
184 self.text = text.into().chars().collect();
185 self.cursor = self.text.len();
186 self
187 }
188
189 pub fn password(mut self) -> Self {
191 self.password = true;
192 self
193 }
194
195 pub fn max_length(mut self, max: usize) -> Self {
197 self.max_length = Some(max);
198 self
199 }
200
201 pub fn numeric(mut self) -> Self {
203 self.numeric = true;
204 self
205 }
206
207 pub fn required(mut self) -> Self {
209 self.allow_empty = false;
210 self
211 }
212
213 pub(crate) fn child(mut self) -> Self {
215 self.child = true;
216 self
217 }
218
219 fn content(&self) -> String {
220 self.text.iter().collect()
221 }
222
223 fn speak_char(&self, c: Option<char>, ui: &mut UiContext<'_>) {
224 let text = match c {
225 None => "blank".to_string(),
226 Some(_) if self.password => "star".to_string(),
227 Some(c) => char_name(c),
228 };
229 ui.speech.speak(text);
230 }
231
232 fn speak_all(&self, ui: &mut UiContext<'_>) {
233 let text = if self.text.is_empty() {
234 "blank".to_string()
235 } else if self.password {
236 format!("{} characters", self.text.len())
237 } else {
238 self.content()
239 };
240 ui.speech.speak(text);
241 }
242
243 fn word_left(&self) -> usize {
244 let mut i = self.cursor;
245 while i > 0 && self.text[i - 1].is_whitespace() {
246 i -= 1;
247 }
248 while i > 0 && !self.text[i - 1].is_whitespace() {
249 i -= 1;
250 }
251 i
252 }
253
254 fn word_right(&self) -> usize {
255 let mut i = self.cursor;
256 while i < self.text.len() && !self.text[i].is_whitespace() {
257 i += 1;
258 }
259 while i < self.text.len() && self.text[i].is_whitespace() {
260 i += 1;
261 }
262 i
263 }
264}
265
266fn char_name(c: char) -> String {
268 match c {
269 ' ' => "space",
270 '.' => "dot",
271 ',' => "comma",
272 '!' => "exclamation",
273 '?' => "question",
274 '-' => "dash",
275 '_' => "underscore",
276 ':' => "colon",
277 ';' => "semicolon",
278 '\'' => "apostrophe",
279 '"' => "quote",
280 '/' => "slash",
281 '\\' => "backslash",
282 '@' => "at",
283 '#' => "number",
284 '(' => "left paren",
285 ')' => "right paren",
286 _ => return c.to_string(),
287 }
288 .to_string()
289}
290
291impl Widget for TextInput {
292 fn id(&self) -> &str {
293 &self.id
294 }
295
296 fn open(&mut self, ui: &mut UiContext<'_>) {
297 ui.play(UiSound::Open, None);
298 let content = if self.text.is_empty() {
299 "blank".to_string()
300 } else if self.password {
301 format!("{} characters", self.text.len())
302 } else {
303 self.content()
304 };
305 ui.speech.speak(format!("{}, edit, {content}", self.prompt));
306 }
307
308 fn update(&mut self, ui: &mut UiContext<'_>) -> Status {
309 let word = ui.input.modifiers().control;
310 if ui.key(Key::Enter) || ui.key(Key::NumpadEnter) {
311 if self.text.is_empty() && !self.allow_empty {
312 ui.play(UiSound::Edge, None);
313 ui.speech.speak("blank");
314 return Status::Open;
315 }
316 ui.play(UiSound::Activate, None);
317 let text = self.content();
318 if self.child {
319 return Status::CloseWith(Value::Text(text));
320 }
321 ui.emit(UiEvent::TextSubmitted { id: self.id.clone(), text });
322 return Status::Close;
323 }
324 if ui.key(Key::Escape) {
325 ui.play(UiSound::Close, None);
326 if !self.child {
327 ui.emit(UiEvent::Closed { id: self.id.clone() });
328 }
329 return Status::Close;
330 }
331 if ui.key(Key::Left) {
332 if self.cursor == 0 {
333 ui.play(UiSound::Edge, None);
334 } else {
335 self.cursor = if word { self.word_left() } else { self.cursor - 1 };
336 }
337 self.speak_char(self.text.get(self.cursor).copied(), ui);
338 } else if ui.key(Key::Right) {
339 if self.cursor >= self.text.len() {
340 ui.play(UiSound::Edge, None);
341 } else {
342 self.cursor = if word { self.word_right() } else { self.cursor + 1 };
343 }
344 self.speak_char(self.text.get(self.cursor).copied(), ui);
345 } else if ui.key(Key::Home) {
346 self.cursor = 0;
347 self.speak_char(self.text.first().copied(), ui);
348 } else if ui.key(Key::End) {
349 self.cursor = self.text.len();
350 self.speak_char(None, ui);
351 } else if ui.key(Key::Up) || ui.key(Key::Down) {
352 self.speak_all(ui);
353 } else if ui.key(Key::Backspace) {
354 if self.cursor == 0 {
355 ui.play(UiSound::Edge, None);
356 } else {
357 self.cursor -= 1;
358 let removed = self.text.remove(self.cursor);
359 ui.play(UiSound::Typing, None);
360 self.speak_char(Some(removed), ui);
361 }
362 } else if ui.key(Key::Delete) {
363 if self.cursor < self.text.len() {
364 self.text.remove(self.cursor);
365 ui.play(UiSound::Typing, None);
366 self.speak_char(self.text.get(self.cursor).copied(), ui);
367 } else {
368 ui.play(UiSound::Edge, None);
369 }
370 }
371
372 let typed: Vec<char> = ui.input.text().chars().collect();
373 let mut last = None;
374 for c in typed {
375 if self.numeric && !(c.is_ascii_digit() || c == '-' || c == '.' || c == ',') {
376 continue;
377 }
378 if self.max_length.is_some_and(|m| self.text.len() >= m) {
379 ui.play(UiSound::Edge, None);
380 break;
381 }
382 self.text.insert(self.cursor, c);
383 self.cursor += 1;
384 last = Some(c);
385 }
386 if last.is_some() {
387 ui.play(UiSound::Typing, None);
388 self.speak_char(last, ui);
389 }
390 Status::Open
391 }
392
393 fn value(&self, _item: &str) -> Option<Value> {
394 Some(Value::Text(self.content()))
395 }
396}