1pub mod glyph;
22
23use crate::prelude::*;
24
25use shuyi_assets::keymap::{ActionSet, UiAction};
26
27use crate::audio::MenuSound;
28use crate::ctx::{Ctx, GameData};
29use crate::input::fixed::{TextKey, text_key};
30use crate::screen::FrameInput;
31
32pub const SPACE: &str = "name_entry.grid.blank";
34pub const DELETE: &str = "name_entry.grid.delete";
36pub const DONE: &str = "name_entry.grid.done";
38pub const TYPE_HINT: &str = "name_entry.type_hint";
40pub const HELP_GRID: &str = "name_entry.help.grid";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct NameRules {
46 pub max_chars: usize,
48 pub digits: bool,
50}
51
52impl NameRules {
53 fn allows(self, name: &str, c: char) -> bool {
55 match c {
56 _ if name.chars().count() >= self.max_chars => false,
57 ' ' => !name.is_empty() && !name.ends_with(' '),
58 c if c.is_ascii_digit() => self.digits,
59 c => c.is_ascii_alphabetic() || c == '-' || c == '\'',
60 }
61 }
62}
63
64fn add_char(name: &mut String, c: char, rules: NameRules) -> bool {
66 let ok = rules.allows(name, c);
67 if ok {
68 name.push(c);
69 }
70 ok
71}
72
73pub fn finished(name: &str) -> Option<String> {
75 let name = name.trim();
76 (!name.is_empty()).then(|| name.to_owned())
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum NameEvent {
82 Open,
84 Done,
86 Cancel,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct NameBox {
93 name: String,
95 rules: NameRules,
96}
97
98impl NameBox {
99 pub fn new(name: &str, rules: NameRules) -> Self {
101 Self {
102 name: name.to_owned(),
103 rules,
104 }
105 }
106
107 pub fn name(&self) -> &str {
109 &self.name
110 }
111
112 pub fn handle<G: GameData>(
116 &mut self,
117 input: &FrameInput<G::Action>,
118 ctx: &mut Ctx<G>,
119 ) -> NameEvent {
120 for &c in input.text() {
121 if !add_char(&mut self.name, c, self.rules) {
122 ctx.audio.menu(MenuSound::Denied);
123 }
124 }
125 for &chord in input.pressed_chords() {
126 match text_key(chord) {
127 Some(TextKey::Done) => match finished(&self.name) {
128 Some(name) => {
129 self.name = name;
130 ctx.audio.menu(MenuSound::Select);
131 return NameEvent::Done;
132 }
133 None => ctx.audio.menu(MenuSound::Denied),
134 },
135 Some(TextKey::Delete) => {
136 if self.name.pop().is_some() {
137 ctx.audio.menu(MenuSound::Cancel);
138 }
139 }
140 Some(TextKey::Cancel) => return NameEvent::Cancel,
141 None => {}
142 }
143 }
144 NameEvent::Open
145 }
146
147 pub fn view<G: GameData>(&self, ctx: &Ctx<G>, title: &str) -> TypingView {
149 TypingView {
150 title: title.to_owned(),
151 typed: self.name.clone(),
152 hint: ctx.text(TYPE_HINT).to_owned(),
153 }
154 }
155}
156
157const LETTER_ROWS: [&str; 4] = [
160 "ABCDEFGHIJKLM",
161 "NOPQRSTUVWXYZ",
162 "abcdefghijklm",
163 "nopqrstuvwxyz",
164];
165
166const DIGIT_ROW: &str = "0123456789";
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum GridCell {
173 Char(char),
175 Space,
177 Delete,
179 Done,
181}
182
183impl GridCell {
184 pub fn label<G: GameData>(self, ctx: &Ctx<G>) -> String {
186 let key = match self {
187 GridCell::Char(c) => return c.to_string(),
188 GridCell::Space => SPACE,
189 GridCell::Delete => DELETE,
190 GridCell::Done => DONE,
191 };
192 ctx.text(key).to_owned()
193 }
194}
195
196pub fn grid(rules: NameRules) -> Vec<Vec<GridCell>> {
199 let chars = |row: &str| row.chars().map(GridCell::Char).collect();
200 let mut rows: Vec<Vec<GridCell>> = LETTER_ROWS.iter().map(|r| chars(r)).collect();
201 if rules.digits {
202 rows.push(chars(DIGIT_ROW));
203 }
204 rows.push(vec![
205 GridCell::Char('-'),
206 GridCell::Char('\''),
207 GridCell::Space,
208 GridCell::Delete,
209 GridCell::Done,
210 ]);
211 rows
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct NameEntry {
217 name: String,
219 at: (usize, usize),
221 rules: NameRules,
222}
223
224impl NameEntry {
225 pub const ACTIONS: &'static [UiAction] = crate::input::NAVIGATION;
229
230 pub fn new(name: &str, rules: NameRules) -> Self {
232 Self {
233 name: name.to_owned(),
234 at: (0, 0),
235 rules,
236 }
237 }
238
239 pub fn name(&self) -> &str {
241 &self.name
242 }
243
244 pub fn focus(&self) -> (usize, usize) {
246 self.at
247 }
248
249 pub fn handle<G: GameData>(&mut self, action: G::Action, ctx: &mut Ctx<G>) -> NameEvent {
255 let rows = grid(self.rules);
256 let (row, col) = self.at;
257 let width = |r: usize| rows[r].len();
258 let moved = match action.ui() {
259 Some(UiAction::Left) => (row, (col + width(row) - 1) % width(row)),
260 Some(UiAction::Right) => (row, (col + 1) % width(row)),
261 Some(UiAction::Up) => {
262 let r = (row + rows.len() - 1) % rows.len();
263 (r, col.min(width(r) - 1))
264 }
265 Some(UiAction::Down) => {
266 let r = (row + 1) % rows.len();
267 (r, col.min(width(r) - 1))
268 }
269 Some(UiAction::Confirm) => return self.confirm(rows[row][col], ctx),
270 Some(UiAction::Cancel) => {
271 if self.name.pop().is_some() {
272 ctx.audio.menu(MenuSound::Cancel);
273 return NameEvent::Open;
274 }
275 return NameEvent::Cancel;
276 }
277 Some(UiAction::Debug) | None => return NameEvent::Open,
278 };
279 if moved != self.at {
280 self.at = moved;
281 ctx.audio.menu(MenuSound::Move);
282 }
283 NameEvent::Open
284 }
285
286 fn confirm<G: GameData>(&mut self, cell: GridCell, ctx: &mut Ctx<G>) -> NameEvent {
290 let ok = match cell {
291 GridCell::Char(c) => add_char(&mut self.name, c, self.rules),
292 GridCell::Space => add_char(&mut self.name, ' ', self.rules),
293 GridCell::Delete => self.name.pop().is_some(),
294 GridCell::Done => {
295 if let Some(name) = finished(&self.name) {
296 self.name = name;
297 ctx.audio.menu(MenuSound::Select);
298 return NameEvent::Done;
299 }
300 false
301 }
302 };
303 ctx.audio.menu(if ok {
304 MenuSound::Select
305 } else {
306 MenuSound::Denied
307 });
308 NameEvent::Open
309 }
310
311 pub fn view<G: GameData>(&self, ctx: &Ctx<G>, title: &str) -> GridView {
313 GridView {
314 title: title.to_owned(),
315 spelled: self.name.clone(),
316 rows: grid(self.rules)
317 .iter()
318 .map(|row| row.iter().map(|g| g.label(ctx)).collect())
319 .collect(),
320 focus: self.at,
321 }
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct TypingView {
328 pub title: String,
330 pub typed: String,
332 pub hint: String,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct GridView {
339 pub title: String,
341 pub spelled: String,
343 pub rows: Vec<Vec<String>>,
345 pub focus: (usize, usize),
347}
348
349#[cfg(test)]
350mod tests;