1use std::fmt;
2
3use thedes_tui::{
4 cancellability::Cancellable,
5 core::App,
6 info::{self, Info},
7 input::{self, Input},
8 menu::{self, Menu},
9};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13#[error("Truncated {} characters from input", .0)]
14pub struct SetNameError(usize);
15
16#[derive(Debug, Error)]
17pub enum InitError {
18 #[error("Failed to initialize menu")]
19 Menu(
20 #[source]
21 #[from]
22 menu::Error,
23 ),
24 #[error("Failed to create name input")]
25 Name(#[source] input::Error),
26 #[error("Failed to create seed input")]
27 Seed(#[source] input::Error),
28}
29
30#[derive(Debug, Error)]
31pub enum Error {
32 #[error("Failed to run menu")]
33 RunMenu(
34 #[source]
35 #[from]
36 menu::Error,
37 ),
38 #[error("Failed to run name input")]
39 RunName(#[source] input::Error),
40 #[error("Failed to run seed input")]
41 RunSeed(#[source] input::Error),
42 #[error("Failed to display information regarding name input")]
43 EmptyNameInfo(#[source] info::Error),
44 #[error("Failed to display information regarding seed input")]
45 EmptySeedInfo(#[source] info::Error),
46}
47
48#[derive(Debug, Clone, PartialEq, Default)]
49pub struct Form {
50 pub name: String,
51 pub seed: Seed,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum NewGameMenuItem {
56 Create,
57 SetName,
58 SetSeed,
59}
60
61impl fmt::Display for NewGameMenuItem {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.write_str(match self {
64 Self::Create => "Create",
65 Self::SetName => "Set Name",
66 Self::SetSeed => "Set Seed",
67 })
68 }
69}
70
71pub type Seed = u32;
72
73#[derive(Debug, Clone)]
74pub struct Component {
75 form: Form,
76 menu: Menu<NewGameMenuItem, Cancellable>,
77 name_input: Input<fn(char) -> bool, Cancellable>,
78 seed_input: Input<fn(char) -> bool, Cancellable>,
79 empty_name_info: Info,
80 empty_seed_info: Info,
81}
82
83impl Component {
84 pub fn new() -> Result<Self, InitError> {
85 let menu = Menu::from_cancellation(
86 "New Game",
87 [
88 NewGameMenuItem::Create,
89 NewGameMenuItem::SetName,
90 NewGameMenuItem::SetSeed,
91 ],
92 Cancellable::new(false),
93 )?;
94
95 let form = Form::default();
96
97 let result = Input::from_cancellation(
98 input::Config {
99 max: 32,
100 title: "New Game's Name",
101 filter: (|ch| {
102 ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'
103 }) as fn(char) -> bool,
104 },
105 Cancellable::new(false),
106 );
107 let name_input = result.map_err(InitError::Name)?;
108
109 let result = Input::from_cancellation(
110 input::Config {
111 max: 8,
112 title: "New Game's Seed",
113 filter: (|ch| ch.is_ascii_hexdigit()) as fn(char) -> bool,
114 },
115 Cancellable::new(false),
116 );
117 let seed_input = result.map_err(InitError::Seed)?;
118
119 let empty_name_info = Info::new("Error!", "Game name cannot be empty");
120 let empty_seed_info = Info::new("Error!", "Game seed cannot be empty");
121
122 Ok(Self {
123 menu,
124 form,
125 name_input,
126 seed_input,
127 empty_name_info,
128 empty_seed_info,
129 })
130 }
131
132 pub fn set_seed(&mut self, seed: Seed) -> &mut Self {
133 self.form.seed = seed;
134 self
135 }
136
137 pub fn set_name(
138 &mut self,
139 name: impl Into<String>,
140 ) -> Result<&mut Self, SetNameError> {
141 let name = name.into();
142 let input_max = usize::from(self.name_input.max());
143 if name.len() > input_max {
144 Err(SetNameError(name.len() - input_max))?
145 }
146 self.form.name = name;
147 Ok(self)
148 }
149
150 pub fn form(&self) -> &Form {
151 &self.form
152 }
153
154 pub fn is_cancelling(&self) -> bool {
155 self.menu.is_cancelling() || self.form.name.is_empty()
156 }
157
158 pub fn output(&self) -> Option<&Form> {
159 if self.is_cancelling() {
160 None?
161 }
162 Some(self.form())
163 }
164
165 fn load_form(&mut self) {
166 let seed_digits = format!("{:x}", self.form.seed);
167 let _ = self.seed_input.set_buffer(seed_digits.chars());
168 let _ = self.name_input.set_buffer(self.form.name.chars());
169 }
170
171 pub async fn run(&mut self, app: &mut App) -> Result<(), Error> {
172 self.load_form();
173 self.read_name(app).await?;
174 if self.name_input.is_cancelling() {
175 self.menu.set_cancelling(true);
176 } else {
177 self.menu.set_selected(0)?;
178 self.menu.set_cancelling(false);
179
180 loop {
181 self.menu.run(app).await?;
182 match self.menu.output() {
183 Some(NewGameMenuItem::Create) => break,
184 Some(NewGameMenuItem::SetName) => {
185 self.read_name(app).await?;
186 },
187 Some(NewGameMenuItem::SetSeed) => {
188 self.read_seed(app).await?;
189 },
190 None => break,
191 }
192 }
193 }
194 Ok(())
195 }
196
197 async fn read_name(&mut self, app: &mut App) -> Result<(), Error> {
198 loop {
199 self.name_input.run(app).await.map_err(Error::RunName)?;
200 let name = self.name_input.output();
201
202 match name {
203 Some(name) if name.is_empty() => {
204 self.empty_name_info
205 .run(app)
206 .await
207 .map_err(Error::EmptyNameInfo)?;
208 },
209 Some(name) => {
210 self.form.name = name;
211 break;
212 },
213 None => break,
214 }
215 }
216 Ok(())
217 }
218
219 async fn read_seed(&mut self, app: &mut App) -> Result<(), Error> {
220 loop {
221 self.seed_input.run(app).await.map_err(Error::RunSeed)?;
222 let seed = self.seed_input.output();
223
224 match seed.map(|s| Seed::from_str_radix(&s, 16)) {
225 Some(Err(_)) => {
226 self.empty_seed_info
227 .run(app)
228 .await
229 .map_err(Error::EmptySeedInfo)?;
230 },
231 Some(Ok(seed)) => {
232 self.form.seed = seed;
233 break;
234 },
235 None => break,
236 }
237 }
238 Ok(())
239 }
240}
241
242#[cfg(test)]
243mod test {
244 use std::time::Duration;
245
246 use thedes_tui::core::{
247 App,
248 event::Key,
249 geometry::CoordPair,
250 runtime::{Config, device::mock::RuntimeDeviceMock},
251 screen,
252 };
253 use thiserror::Error;
254 use tokio::{task, time::timeout};
255
256 #[derive(Debug, Error)]
257 enum Error {
258 #[error(transparent)]
259 Init(#[from] super::InitError),
260 #[error(transparent)]
261 Run(#[from] super::Error),
262 }
263
264 async fn tui_main(mut app: App) -> Result<Option<super::Form>, Error> {
265 let mut component = super::Component::new()?;
266 component.run(&mut app).await?;
267 Ok(component.output().cloned())
268 }
269
270 #[tokio::test(flavor = "multi_thread")]
271 async fn cancel_first_name_input() {
272 let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
273 let device = device_mock.open();
274 let config = Config::new()
275 .with_screen(
276 screen::Config::new()
277 .with_canvas_size(CoordPair { y: 22, x: 78 }),
278 )
279 .with_device(device);
280
281 device_mock.input().publish_ok([Key::Esc]);
282
283 let runtime_future = task::spawn(config.run(tui_main));
284 let output = timeout(Duration::from_millis(200), runtime_future)
285 .await
286 .unwrap()
287 .unwrap()
288 .unwrap()
289 .unwrap();
290 assert_eq!(output, None);
291 }
292
293 #[tokio::test(flavor = "multi_thread")]
294 async fn confirm_default_seed() {
295 let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
296 let device = device_mock.open();
297 let config = Config::new()
298 .with_screen(
299 screen::Config::new()
300 .with_canvas_size(CoordPair { y: 22, x: 78 }),
301 )
302 .with_device(device);
303
304 device_mock.input().publish_ok([
305 Key::Char('w'),
306 Key::Char('0'),
307 Key::Enter,
308 ]);
309
310 let runtime_future = task::spawn(config.run(tui_main));
311 tokio::time::sleep(Duration::from_millis(50)).await;
312 device_mock.input().publish_ok([Key::Enter]);
313
314 let output = timeout(Duration::from_millis(200), runtime_future)
315 .await
316 .unwrap()
317 .unwrap()
318 .unwrap()
319 .unwrap();
320 assert_eq!(
321 output,
322 Some(super::Form { name: "w0".to_owned(), seed: 0 })
323 );
324 }
325
326 #[tokio::test(flavor = "multi_thread")]
327 async fn cancel_menu() {
328 let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
329 let device = device_mock.open();
330 let config = Config::new()
331 .with_screen(
332 screen::Config::new()
333 .with_canvas_size(CoordPair { y: 22, x: 78 }),
334 )
335 .with_device(device);
336
337 device_mock.input().publish_ok([
338 Key::Char('w'),
339 Key::Char('0'),
340 Key::Enter,
341 ]);
342
343 let runtime_future = task::spawn(config.run(tui_main));
344 tokio::time::sleep(Duration::from_millis(50)).await;
345 device_mock.input().publish_ok([Key::Esc]);
346
347 let output = timeout(Duration::from_millis(200), runtime_future)
348 .await
349 .unwrap()
350 .unwrap()
351 .unwrap()
352 .unwrap();
353 assert_eq!(output, None);
354 }
355
356 #[tokio::test(flavor = "multi_thread")]
357 async fn confirm_custom_seed() {
358 let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
359 let device = device_mock.open();
360 let config = Config::new()
361 .with_screen(
362 screen::Config::new()
363 .with_canvas_size(CoordPair { y: 22, x: 78 }),
364 )
365 .with_device(device);
366
367 device_mock.input().publish_ok([
368 Key::Char('w'),
369 Key::Char('0'),
370 Key::Enter,
371 ]);
372
373 let runtime_future = task::spawn(config.run(tui_main));
374
375 tokio::time::sleep(Duration::from_millis(50)).await;
376 device_mock.input().publish_ok([Key::Down, Key::Down, Key::Enter]);
377 tokio::time::sleep(Duration::from_millis(50)).await;
378 device_mock.input().publish_ok([
379 Key::Char('5'),
380 Key::Char('a'),
381 Key::Char('9'),
382 Key::Enter,
383 ]);
384
385 tokio::time::sleep(Duration::from_millis(50)).await;
386 device_mock.input().publish_ok([Key::Up, Key::Up, Key::Enter]);
387
388 let output = timeout(Duration::from_millis(200), runtime_future)
389 .await
390 .unwrap()
391 .unwrap()
392 .unwrap()
393 .unwrap();
394 assert_eq!(
395 output,
396 Some(super::Form { name: "w0".to_owned(), seed: 0x5a9 })
397 );
398 }
399
400 #[tokio::test(flavor = "multi_thread")]
401 async fn do_not_allow_empty_name() {
402 let device_mock = RuntimeDeviceMock::new(CoordPair { y: 24, x: 80 });
403 let device = device_mock.open();
404 let config = Config::new()
405 .with_screen(
406 screen::Config::new()
407 .with_canvas_size(CoordPair { y: 22, x: 78 }),
408 )
409 .with_device(device);
410
411 let runtime_future = task::spawn(config.run(tui_main));
412 tokio::time::sleep(Duration::from_millis(50)).await;
413
414 device_mock.input().publish_ok([Key::Enter]);
415 tokio::time::sleep(Duration::from_millis(50)).await;
416
417 device_mock.input().publish_ok([Key::Enter]);
418 tokio::time::sleep(Duration::from_millis(50)).await;
419
420 device_mock.input().publish_ok([
421 Key::Char('w'),
422 Key::Char('0'),
423 Key::Enter,
424 ]);
425 tokio::time::sleep(Duration::from_millis(50)).await;
426
427 device_mock.input().publish_ok([Key::Enter]);
428
429 let output = timeout(Duration::from_millis(200), runtime_future)
430 .await
431 .unwrap()
432 .unwrap()
433 .unwrap()
434 .unwrap();
435 assert_eq!(
436 output,
437 Some(super::Form { name: "w0".to_owned(), seed: 0 })
438 );
439 }
440}