shuyi_ui/system_keyboard.rs
1//! The system's own keyboard for a text box (ticket 3003): a console's, or
2//! Steam's on-screen one on the Steam Deck. A screen asks for it through
3//! [`Ctx::system_keyboard`](crate::Ctx::system_keyboard), the platform's
4//! `App` opens it (`shuyi_platform::TextInput`), and the player's answer
5//! comes back in a later frame. Where the platform has none, a screen reads
6//! typed keys ([`FrameInput::text`](crate::FrameInput::text)) or spells
7//! on a letter grid, as before.
8//!
9//! The handshake, one frame at a time:
10//!
11//! 1. before each frame the platform says whether it has a keyboard
12//! ([`set_present`](SystemKeyboard::set_present));
13//! 2. a screen [`ask`](SystemKeyboard::ask)s;
14//! 3. after the frame the platform takes the ask
15//! ([`take_ask`](SystemKeyboard::take_ask)) and opens its keyboard;
16//! 4. once the player closes it, the platform hands back the answer
17//! ([`answer`](SystemKeyboard::answer)) before a frame, and the screen
18//! takes it ([`take_answer`](SystemKeyboard::take_answer)).
19
20use crate::prelude::*;
21
22/// What a text box asks the system keyboard for.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct TextAsk {
25 /// What the text is for, shown with the keyboard ("Name"), already in
26 /// the player's language.
27 pub prompt: String,
28 /// The text so far, which the player starts from.
29 pub text: String,
30 /// The most characters the box takes.
31 pub max_chars: u16,
32}
33
34/// What the player did with the system keyboard.
35#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36pub enum TextAnswer {
37 /// They typed this and confirmed it: the whole text, not a change.
38 Typed(String),
39 /// They closed it without confirming, or it couldn't open.
40 Cancelled,
41}
42
43/// The system keyboard as the screens see it: whether there is one, the
44/// ask waiting for the platform and the answer waiting for the screen.
45/// Absent until the platform says otherwise.
46#[derive(Debug, Clone, Default, PartialEq, Eq)]
47pub struct SystemKeyboard {
48 present: bool,
49 /// Asked for, not yet taken by the platform.
50 asked: Option<TextAsk>,
51 /// Taken by the platform, not yet answered.
52 open: bool,
53 /// Answered, not yet taken by the screen.
54 answer: Option<TextAnswer>,
55}
56
57impl SystemKeyboard {
58 /// Whether the platform has a system keyboard.
59 pub fn is_present(&self) -> bool {
60 self.present
61 }
62
63 /// Asks for the keyboard with `ask`. Without one nothing is asked and
64 /// this is `false`: the screen takes typed keys instead. Asking again
65 /// before the platform opened it replaces the ask; asking while it is
66 /// open does nothing, and is `false`.
67 pub fn ask(&mut self, ask: TextAsk) -> bool {
68 if !self.present || self.open {
69 return false;
70 }
71 self.asked = Some(ask);
72 true
73 }
74
75 /// Whether an ask waits for the player: asked for and not yet
76 /// answered.
77 pub fn is_waiting(&self) -> bool {
78 self.asked.is_some() || self.open
79 }
80
81 /// The player's answer, once: `None` until they close the keyboard.
82 pub fn take_answer(&mut self) -> Option<TextAnswer> {
83 self.answer.take()
84 }
85
86 /// The platform says whether it has a keyboard, before each frame. One
87 /// that goes away takes an ask with it: the screen hears
88 /// [`TextAnswer::Cancelled`].
89 pub fn set_present(&mut self, present: bool) {
90 self.present = present;
91 if !present && self.is_waiting() {
92 self.answer(TextAnswer::Cancelled);
93 }
94 }
95
96 /// The ask to open, once, for the platform: from then on the keyboard
97 /// is open until [`answer`](Self::answer).
98 pub fn take_ask(&mut self) -> Option<TextAsk> {
99 let ask = self.asked.take()?;
100 self.open = true;
101 Some(ask)
102 }
103
104 /// The platform hands back what the player did, which closes the
105 /// keyboard.
106 pub fn answer(&mut self, answer: TextAnswer) {
107 self.asked = None;
108 self.open = false;
109 self.answer = Some(answer);
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn name(text: &str) -> TextAsk {
118 TextAsk {
119 prompt: "Name".into(),
120 text: text.into(),
121 max_chars: 12,
122 }
123 }
124
125 #[test]
126 fn without_a_keyboard_nothing_is_asked() {
127 let mut keyboard = SystemKeyboard::default();
128 assert!(!keyboard.is_present());
129 assert!(!keyboard.ask(name("")));
130 assert!(!keyboard.is_waiting());
131 assert_eq!(keyboard.take_ask(), None);
132 }
133
134 #[test]
135 fn an_ask_goes_to_the_platform_and_its_answer_back_once() {
136 let mut keyboard = SystemKeyboard::default();
137 keyboard.set_present(true);
138 assert!(keyboard.is_present());
139 assert!(keyboard.ask(name("A")));
140 // Asked again before the platform took it: the later ask wins.
141 assert!(keyboard.ask(name("Ab")));
142 assert!(keyboard.is_waiting());
143 assert_eq!(keyboard.take_ask(), Some(name("Ab")));
144 assert_eq!(keyboard.take_ask(), None);
145 // Open: a new ask waits for this one's answer.
146 assert!(keyboard.is_waiting());
147 assert!(!keyboard.ask(name("C")));
148 assert_eq!(keyboard.take_answer(), None);
149 keyboard.answer(TextAnswer::Typed("Abe".into()));
150 assert!(!keyboard.is_waiting());
151 assert_eq!(
152 keyboard.take_answer(),
153 Some(TextAnswer::Typed("Abe".into()))
154 );
155 assert_eq!(keyboard.take_answer(), None);
156 // Closed: it can be asked for again.
157 assert!(keyboard.ask(name("Abe")));
158 }
159
160 #[test]
161 fn a_keyboard_that_goes_away_cancels_what_waits() {
162 for open in [false, true] {
163 let mut keyboard = SystemKeyboard::default();
164 keyboard.set_present(true);
165 assert!(keyboard.ask(name("")));
166 if open {
167 assert!(keyboard.take_ask().is_some());
168 }
169 keyboard.set_present(false);
170 assert!(!keyboard.is_waiting(), "open: {open}");
171 assert_eq!(keyboard.take_ask(), None, "open: {open}");
172 assert_eq!(keyboard.take_answer(), Some(TextAnswer::Cancelled));
173 }
174 // With nothing waiting, nothing is answered.
175 let mut keyboard = SystemKeyboard::default();
176 keyboard.set_present(true);
177 keyboard.set_present(false);
178 assert_eq!(keyboard.take_answer(), None);
179 }
180}