Skip to main content

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}