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: Option<String>,
72 no: Option<String>,
73 selected_yes: bool,
74}
75
76impl Confirm {
77 pub fn new(id: impl Into<String>, question: impl Into<String>) -> Self {
80 Self { id: id.into(), question: question.into(), yes: None, no: None, selected_yes: true }
81 }
82
83 pub fn labels(mut self, yes: impl Into<String>, no: impl Into<String>) -> Self {
85 self.yes = Some(yes.into());
86 self.no = Some(no.into());
87 self
88 }
89
90 pub fn default_no(mut self) -> Self {
92 self.selected_yes = false;
93 self
94 }
95
96 fn label(&self, yes: bool, ui: &UiContext<'_>) -> String {
97 let (custom, key) = if yes { (&self.yes, "ui.yes") } else { (&self.no, "ui.no") };
98 custom.clone().unwrap_or_else(|| ui.lang.get(key))
99 }
100
101 fn current(&self, ui: &UiContext<'_>) -> String {
102 self.label(self.selected_yes, ui)
103 }
104
105 fn answer(&self, yes: bool, ui: &mut UiContext<'_>) -> Status {
106 ui.play(if yes { UiSound::Activate } else { UiSound::Close }, None);
107 ui.emit(UiEvent::Confirmed { id: self.id.clone(), yes });
108 Status::Close
109 }
110}
111
112fn first_lower(s: &str) -> Option<char> {
113 s.chars().next().and_then(|c| c.to_lowercase().next())
114}
115
116impl Widget for Confirm {
117 fn id(&self) -> &str {
118 &self.id
119 }
120
121 fn open(&mut self, ui: &mut UiContext<'_>) {
122 ui.play(UiSound::Open, None);
123 let answer = self.current(ui);
124 ui.speech.speak(format!("{} {answer}", self.question));
125 }
126
127 fn update(&mut self, ui: &mut UiContext<'_>) -> Status {
128 if ui.key(Key::Up) || ui.key(Key::Down) || ui.key(Key::Left) || ui.key(Key::Right) || ui.key(Key::Tab) {
129 self.selected_yes = !self.selected_yes;
130 ui.play(UiSound::Navigate, None);
131 let text = self.current(ui);
132 ui.speech.speak(text);
133 } else if ui.key(Key::Enter) || ui.key(Key::NumpadEnter) || ui.input.pressed(Key::Space) {
134 return self.answer(self.selected_yes, ui);
135 } else if ui.key(Key::Escape) {
136 return self.answer(false, ui);
137 } else if let Some(c) = ui.input.text().chars().next().and_then(|c| c.to_lowercase().next()) {
138 if Some(c) == first_lower(&self.label(true, ui)) {
139 return self.answer(true, ui);
140 }
141 if Some(c) == first_lower(&self.label(false, ui)) {
142 return self.answer(false, ui);
143 }
144 }
145 Status::Open
146 }
147}
148
149#[derive(Debug, Clone)]
162pub struct TextInput {
163 id: String,
164 prompt: String,
165 text: Vec<char>,
166 cursor: usize,
167 password: bool,
168 max_length: Option<usize>,
169 numeric: bool,
170 allow_empty: bool,
171 child: bool,
172}
173
174impl TextInput {
175 pub fn new(id: impl Into<String>, prompt: impl Into<String>) -> Self {
177 Self {
178 id: id.into(),
179 prompt: prompt.into(),
180 text: Vec::new(),
181 cursor: 0,
182 password: false,
183 max_length: None,
184 numeric: false,
185 allow_empty: true,
186 child: false,
187 }
188 }
189
190 pub fn initial(mut self, text: impl Into<String>) -> Self {
192 self.text = text.into().chars().collect();
193 self.cursor = self.text.len();
194 self
195 }
196
197 pub fn password(mut self) -> Self {
199 self.password = true;
200 self
201 }
202
203 pub fn max_length(mut self, max: usize) -> Self {
205 self.max_length = Some(max);
206 self
207 }
208
209 pub fn numeric(mut self) -> Self {
211 self.numeric = true;
212 self
213 }
214
215 pub fn required(mut self) -> Self {
217 self.allow_empty = false;
218 self
219 }
220
221 pub(crate) fn child(mut self) -> Self {
223 self.child = true;
224 self
225 }
226
227 fn content(&self) -> String {
228 self.text.iter().collect()
229 }
230
231 fn speak_char(&self, c: Option<char>, ui: &mut UiContext<'_>) {
232 let text = match c {
233 None => ui.lang.get("ui.blank"),
234 Some(_) if self.password => ui.lang.get("ui.star"),
235 Some(c) => char_name(c, ui),
236 };
237 ui.speech.speak(text);
238 }
239
240 fn speak_all(&self, ui: &mut UiContext<'_>) {
241 let text = if self.text.is_empty() {
242 ui.lang.get("ui.blank")
243 } else if self.password {
244 ui.lang.plural("ui.characters", self.text.len() as i64)
245 } else {
246 self.content()
247 };
248 ui.speech.speak(text);
249 }
250
251 fn word_left(&self) -> usize {
252 let mut i = self.cursor;
253 while i > 0 && self.text[i - 1].is_whitespace() {
254 i -= 1;
255 }
256 while i > 0 && !self.text[i - 1].is_whitespace() {
257 i -= 1;
258 }
259 i
260 }
261
262 fn word_right(&self) -> usize {
263 let mut i = self.cursor;
264 while i < self.text.len() && !self.text[i].is_whitespace() {
265 i += 1;
266 }
267 while i < self.text.len() && self.text[i].is_whitespace() {
268 i += 1;
269 }
270 i
271 }
272}
273
274fn char_name(c: char, ui: &UiContext<'_>) -> String {
276 let key = match c {
277 ' ' => "char.space",
278 '.' => "char.dot",
279 ',' => "char.comma",
280 '!' => "char.exclamation",
281 '?' => "char.question",
282 '-' => "char.dash",
283 '_' => "char.underscore",
284 ':' => "char.colon",
285 ';' => "char.semicolon",
286 '\'' => "char.apostrophe",
287 '"' => "char.quote",
288 '/' => "char.slash",
289 '\\' => "char.backslash",
290 '@' => "char.at",
291 '#' => "char.number",
292 '(' => "char.left_paren",
293 ')' => "char.right_paren",
294 _ => return c.to_string(),
295 };
296 ui.lang.get(key)
297}
298
299impl Widget for TextInput {
300 fn id(&self) -> &str {
301 &self.id
302 }
303
304 fn open(&mut self, ui: &mut UiContext<'_>) {
305 ui.play(UiSound::Open, None);
306 let content = if self.text.is_empty() {
307 ui.lang.get("ui.blank")
308 } else if self.password {
309 ui.lang.plural("ui.characters", self.text.len() as i64)
310 } else {
311 self.content()
312 };
313 let text = ui.lang.format("ui.edit", &[("label", &self.prompt), ("value", &content)]);
314 ui.speech.speak(text);
315 }
316
317 fn update(&mut self, ui: &mut UiContext<'_>) -> Status {
318 let word = ui.input.modifiers().control;
319 if ui.key(Key::Enter) || ui.key(Key::NumpadEnter) {
320 if self.text.is_empty() && !self.allow_empty {
321 ui.play(UiSound::Edge, None);
322 let blank = ui.lang.get("ui.blank");
323 ui.speech.speak(blank);
324 return Status::Open;
325 }
326 ui.play(UiSound::Activate, None);
327 let text = self.content();
328 if self.child {
329 return Status::CloseWith(Value::Text(text));
330 }
331 ui.emit(UiEvent::TextSubmitted { id: self.id.clone(), text });
332 return Status::Close;
333 }
334 if ui.key(Key::Escape) {
335 ui.play(UiSound::Close, None);
336 if !self.child {
337 ui.emit(UiEvent::Closed { id: self.id.clone() });
338 }
339 return Status::Close;
340 }
341 if ui.key(Key::Left) {
342 if self.cursor == 0 {
343 ui.play(UiSound::Edge, None);
344 } else {
345 self.cursor = if word { self.word_left() } else { self.cursor - 1 };
346 }
347 self.speak_char(self.text.get(self.cursor).copied(), ui);
348 } else if ui.key(Key::Right) {
349 if self.cursor >= self.text.len() {
350 ui.play(UiSound::Edge, None);
351 } else {
352 self.cursor = if word { self.word_right() } else { self.cursor + 1 };
353 }
354 self.speak_char(self.text.get(self.cursor).copied(), ui);
355 } else if ui.key(Key::Home) {
356 self.cursor = 0;
357 self.speak_char(self.text.first().copied(), ui);
358 } else if ui.key(Key::End) {
359 self.cursor = self.text.len();
360 self.speak_char(None, ui);
361 } else if ui.key(Key::Up) || ui.key(Key::Down) {
362 self.speak_all(ui);
363 } else if ui.key(Key::Backspace) {
364 if self.cursor == 0 {
365 ui.play(UiSound::Edge, None);
366 } else {
367 self.cursor -= 1;
368 let removed = self.text.remove(self.cursor);
369 ui.play(UiSound::Typing, None);
370 self.speak_char(Some(removed), ui);
371 }
372 } else if ui.key(Key::Delete) {
373 if self.cursor < self.text.len() {
374 self.text.remove(self.cursor);
375 ui.play(UiSound::Typing, None);
376 self.speak_char(self.text.get(self.cursor).copied(), ui);
377 } else {
378 ui.play(UiSound::Edge, None);
379 }
380 }
381
382 let typed: Vec<char> = ui.input.text().chars().collect();
383 let mut last = None;
384 for c in typed {
385 if self.numeric && !(c.is_ascii_digit() || c == '-' || c == '.' || c == ',') {
386 continue;
387 }
388 if self.max_length.is_some_and(|m| self.text.len() >= m) {
389 ui.play(UiSound::Edge, None);
390 break;
391 }
392 self.text.insert(self.cursor, c);
393 self.cursor += 1;
394 last = Some(c);
395 }
396 if last.is_some() {
397 ui.play(UiSound::Typing, None);
398 self.speak_char(last, ui);
399 }
400 Status::Open
401 }
402
403 fn value(&self, _item: &str) -> Option<Value> {
404 Some(Value::Text(self.content()))
405 }
406}