1use core::fmt;
39use core::str::FromStr;
40
41use crcbl_core::input::{KeyCode, PointerButton};
42
43use crate::{Binding, Modifier, PadButton, PointerAxis, Stick, Trigger};
44
45#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct BindingParseError {
49 pub text: String,
51 pub reason: &'static str,
53}
54
55impl fmt::Display for BindingParseError {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{:?} is not a binding: {}", self.text, self.reason)
58 }
59}
60
61impl std::error::Error for BindingParseError {}
62
63impl fmt::Display for Binding {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Key(key) => f.write_str(key.as_str()),
67 Self::Chord { modifier, key } => {
68 write!(f, "{}+{}", modifier_name(*modifier), key.as_str())
69 }
70 Self::ScrollChord { held } => write!(f, "{}+Scroll", held.as_str()),
71 Self::MouseButton(button) => write_mouse_button(f, *button),
72 Self::ButtonChord { modifier, button } => {
73 write!(f, "{}+", modifier_name(*modifier))?;
74 write_mouse_button(f, *button)
75 }
76 Self::MouseMotion => f.write_str("MouseMotion"),
77 Self::MouseScroll => f.write_str("MouseScroll"),
78 Self::PointerPosition { axis } => match axis {
79 PointerAxis::X => f.write_str("Pointer:X"),
80 PointerAxis::Y => f.write_str("Pointer:Y"),
81 },
82 Self::KeyAxis { negative, positive } => {
83 write!(f, "KeyAxis:{},{}", negative.as_str(), positive.as_str())
84 }
85 Self::Wasd {
86 up,
87 down,
88 left,
89 right,
90 } => write!(
91 f,
92 "Wasd:{},{},{},{}",
93 up.as_str(),
94 down.as_str(),
95 left.as_str(),
96 right.as_str()
97 ),
98 Self::Virtual(id) => write!(f, "Virtual:{id}"),
99 Self::PadButton(button) => write!(f, "Pad:{}", pad_button_name(*button)),
100 Self::PadChord { modifier, button } => write!(
101 f,
102 "Pad:{}+{}",
103 pad_button_name(*modifier),
104 pad_button_name(*button)
105 ),
106 Self::PadDpad => f.write_str("Pad:Dpad"),
107 Self::PadStick { stick, deadzone } => {
108 write!(f, "PadStick:{}>{deadzone}", stick_name(*stick))
109 }
110 Self::PadTrigger { trigger, threshold } => {
111 write!(f, "PadTrigger:{}>{threshold}", trigger_name(*trigger))
112 }
113 }
114 }
115}
116
117impl FromStr for Binding {
118 type Err = BindingParseError;
119
120 fn from_str(text: &str) -> Result<Self, Self::Err> {
121 let fail = |reason| BindingParseError {
122 text: text.to_owned(),
123 reason,
124 };
125 match text {
126 "MouseMotion" => return Ok(Self::MouseMotion),
127 "MouseScroll" => return Ok(Self::MouseScroll),
128 "Pad:Dpad" => return Ok(Self::PadDpad),
129 _ => {}
130 }
131 if let Some((kind, rest)) = text.split_once(':') {
132 if let Some(modifier) = kind.strip_suffix("+Mouse") {
133 let modifier = modifier_named(modifier).ok_or_else(|| fail("no such modifier"))?;
134 let button =
135 mouse_button_named(rest).ok_or_else(|| fail("no such mouse button"))?;
136 return Ok(Self::ButtonChord { modifier, button });
137 }
138 return match kind {
139 "Mouse" => mouse_button_named(rest)
140 .map(Self::MouseButton)
141 .ok_or_else(|| fail("no such mouse button")),
142 "Pointer" => match rest {
143 "X" => Ok(Self::PointerPosition {
144 axis: PointerAxis::X,
145 }),
146 "Y" => Ok(Self::PointerPosition {
147 axis: PointerAxis::Y,
148 }),
149 _ => Err(fail("a pointer axis is X or Y")),
150 },
151 "KeyAxis" => match keys::<2>(rest) {
152 Some([negative, positive]) => Ok(Self::KeyAxis { negative, positive }),
153 None => Err(fail("a key axis is two key names")),
154 },
155 "Wasd" => match keys::<4>(rest) {
156 Some([up, down, left, right]) => Ok(Self::Wasd {
157 up,
158 down,
159 left,
160 right,
161 }),
162 None => Err(fail("a wasd binding is four key names")),
163 },
164 "Virtual" => Ok(Self::Virtual(rest.to_owned())),
165 "Pad" => match rest.split_once('+') {
166 Some((modifier, button)) => pad_button_named(modifier)
167 .zip(pad_button_named(button))
168 .map(|(modifier, button)| Self::PadChord { modifier, button })
169 .ok_or_else(|| fail("a pad chord is two pad buttons")),
170 None => pad_button_named(rest)
171 .map(Self::PadButton)
172 .ok_or_else(|| fail("no such pad button")),
173 },
174 "PadStick" => {
175 let (stick, deadzone) = named_level(rest, stick_named)
176 .ok_or_else(|| fail("a stick is Left or Right, then > and a dead zone"))?;
177 Ok(Self::PadStick { stick, deadzone })
178 }
179 "PadTrigger" => {
180 let (trigger, threshold) =
181 named_level(rest, trigger_named).ok_or_else(|| {
182 fail("a trigger is Left or Right, then > and a threshold")
183 })?;
184 Ok(Self::PadTrigger { trigger, threshold })
185 }
186 _ => Err(fail("no such binding kind")),
187 };
188 }
189 if let Some((first, second)) = text.split_once('+') {
190 if second == "Scroll" {
191 return KeyCode::from_name(first)
192 .map(|held| Self::ScrollChord { held })
193 .ok_or_else(|| fail("no such key before +Scroll"));
194 }
195 let modifier = modifier_named(first).ok_or_else(|| fail("no such modifier"))?;
196 let key = KeyCode::from_name(second).ok_or_else(|| fail("no such key"))?;
197 return Ok(Self::Chord { modifier, key });
198 }
199 KeyCode::from_name(text)
200 .map(Self::Key)
201 .ok_or_else(|| fail("no such key"))
202 }
203}
204
205fn keys<const N: usize>(text: &str) -> Option<[KeyCode; N]> {
207 let mut out = [KeyCode::KeyA; N];
208 let mut names = text.split(',');
209 for slot in &mut out {
210 *slot = KeyCode::from_name(names.next()?)?;
211 }
212 names.next().is_none().then_some(out)
213}
214
215fn named_level<T>(text: &str, named: fn(&str) -> Option<T>) -> Option<(T, f32)> {
217 let (name, level) = text.split_once('>')?;
218 Some((named(name)?, level.parse().ok()?))
219}
220
221const fn modifier_name(modifier: Modifier) -> &'static str {
222 match modifier {
223 Modifier::Shift => "Shift",
224 Modifier::Control => "Control",
225 Modifier::Alt => "Alt",
226 Modifier::Super => "Super",
227 }
228}
229
230fn modifier_named(name: &str) -> Option<Modifier> {
231 [
232 Modifier::Shift,
233 Modifier::Control,
234 Modifier::Alt,
235 Modifier::Super,
236 ]
237 .into_iter()
238 .find(|modifier| modifier_name(*modifier) == name)
239}
240
241fn write_mouse_button(f: &mut fmt::Formatter<'_>, button: PointerButton) -> fmt::Result {
245 match button {
246 PointerButton::Other(index) => write!(f, "Mouse:{index}"),
247 named => write!(f, "Mouse:{}", pointer_button_name(named)),
248 }
249}
250
251fn mouse_button_named(text: &str) -> Option<PointerButton> {
253 text.parse()
254 .map(PointerButton::Other)
255 .ok()
256 .or_else(|| pointer_button_named(text))
257}
258
259const fn pointer_button_name(button: PointerButton) -> &'static str {
261 match button {
262 PointerButton::Left => "Left",
263 PointerButton::Right => "Right",
264 PointerButton::Middle => "Middle",
265 PointerButton::Back => "Back",
266 PointerButton::Forward => "Forward",
267 PointerButton::Other(_) => "Other",
268 }
269}
270
271fn pointer_button_named(name: &str) -> Option<PointerButton> {
272 [
273 PointerButton::Left,
274 PointerButton::Right,
275 PointerButton::Middle,
276 PointerButton::Back,
277 PointerButton::Forward,
278 ]
279 .into_iter()
280 .find(|button| pointer_button_name(*button) == name)
281}
282
283const fn pad_button_name(button: PadButton) -> &'static str {
284 match button {
285 PadButton::South => "South",
286 PadButton::East => "East",
287 PadButton::West => "West",
288 PadButton::North => "North",
289 PadButton::LeftShoulder => "LeftShoulder",
290 PadButton::RightShoulder => "RightShoulder",
291 PadButton::LeftStick => "LeftStick",
292 PadButton::RightStick => "RightStick",
293 PadButton::Start => "Start",
294 PadButton::Select => "Select",
295 PadButton::DpadUp => "DpadUp",
296 PadButton::DpadDown => "DpadDown",
297 PadButton::DpadLeft => "DpadLeft",
298 PadButton::DpadRight => "DpadRight",
299 PadButton::Guide => "Guide",
300 }
301}
302
303fn pad_button_named(name: &str) -> Option<PadButton> {
304 PadButton::ALL
305 .into_iter()
306 .find(|button| pad_button_name(*button) == name)
307}
308
309pub(crate) const fn stick_name(stick: Stick) -> &'static str {
310 match stick {
311 Stick::Left => "Left",
312 Stick::Right => "Right",
313 }
314}
315
316fn stick_named(name: &str) -> Option<Stick> {
317 [Stick::Left, Stick::Right]
318 .into_iter()
319 .find(|stick| stick_name(*stick) == name)
320}
321
322pub(crate) const fn trigger_name(trigger: Trigger) -> &'static str {
323 match trigger {
324 Trigger::Left => "Left",
325 Trigger::Right => "Right",
326 }
327}
328
329fn trigger_named(name: &str) -> Option<Trigger> {
330 [Trigger::Left, Trigger::Right]
331 .into_iter()
332 .find(|trigger| trigger_name(*trigger) == name)
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn every_binding() -> Vec<Binding> {
341 let mut all = vec![
342 Binding::MouseMotion,
343 Binding::MouseScroll,
344 Binding::PointerPosition {
345 axis: PointerAxis::X,
346 },
347 Binding::PointerPosition {
348 axis: PointerAxis::Y,
349 },
350 Binding::KeyAxis {
351 negative: KeyCode::KeyS,
352 positive: KeyCode::KeyW,
353 },
354 Binding::Wasd {
355 up: KeyCode::ArrowUp,
356 down: KeyCode::ArrowDown,
357 left: KeyCode::ArrowLeft,
358 right: KeyCode::ArrowRight,
359 },
360 Binding::Virtual("fire".to_owned()),
361 Binding::Virtual("odd:id+with>marks,".to_owned()),
362 Binding::Virtual(String::new()),
363 Binding::PadDpad,
364 Binding::MouseButton(PointerButton::Other(9)),
365 ];
366 all.extend(KeyCode::ALL.iter().map(|key| Binding::Key(*key)));
367 all.extend(
368 KeyCode::ALL
369 .iter()
370 .map(|key| Binding::ScrollChord { held: *key }),
371 );
372 for modifier in [
373 Modifier::Shift,
374 Modifier::Control,
375 Modifier::Alt,
376 Modifier::Super,
377 ] {
378 all.push(Binding::Chord {
379 modifier,
380 key: KeyCode::Tab,
381 });
382 }
383 all.extend(
384 [
385 PointerButton::Left,
386 PointerButton::Right,
387 PointerButton::Middle,
388 PointerButton::Back,
389 PointerButton::Forward,
390 ]
391 .map(Binding::MouseButton),
392 );
393 for button in [
394 PointerButton::Left,
395 PointerButton::Right,
396 PointerButton::Middle,
397 PointerButton::Back,
398 PointerButton::Forward,
399 PointerButton::Other(9),
400 ] {
401 all.push(Binding::ButtonChord {
402 modifier: Modifier::Alt,
403 button,
404 });
405 }
406 all.extend(
407 [Modifier::Shift, Modifier::Control, Modifier::Super].map(|modifier| {
408 Binding::ButtonChord {
409 modifier,
410 button: PointerButton::Right,
411 }
412 }),
413 );
414 all.extend(PadButton::ALL.map(Binding::PadButton));
415 all.extend(PadButton::ALL.map(|button| Binding::PadChord {
416 modifier: PadButton::LeftShoulder,
417 button,
418 }));
419 for (stick, trigger) in [(Stick::Left, Trigger::Left), (Stick::Right, Trigger::Right)] {
420 for level in [0.0, 0.1, 0.25, 1.0 / 3.0, 0.999_999_9] {
421 all.push(Binding::PadStick {
422 stick,
423 deadzone: level,
424 });
425 all.push(Binding::PadTrigger {
426 trigger,
427 threshold: level,
428 });
429 }
430 }
431 all
432 }
433
434 #[test]
435 fn every_binding_reads_back_as_itself() {
436 let all = every_binding();
437 for binding in &all {
438 let text = binding.to_string();
439 assert_eq!(
440 text.parse::<Binding>().as_ref(),
441 Ok(binding),
442 "{binding:?} printed as {text:?}"
443 );
444 }
445 let kinds: std::collections::HashSet<_> = all.iter().map(std::mem::discriminant).collect();
448 assert_eq!(kinds.len(), 16, "one of each Binding variant");
449 }
450
451 #[test]
452 fn the_spelling_is_the_documented_one() {
453 for (binding, text) in [
454 (Binding::Key(KeyCode::KeyR), "KeyR"),
455 (
456 Binding::Chord {
457 modifier: Modifier::Alt,
458 key: KeyCode::KeyR,
459 },
460 "Alt+KeyR",
461 ),
462 (
463 Binding::ScrollChord {
464 held: KeyCode::ControlLeft,
465 },
466 "ControlLeft+Scroll",
467 ),
468 (Binding::MouseButton(PointerButton::Left), "Mouse:Left"),
469 (
470 Binding::ButtonChord {
471 modifier: Modifier::Alt,
472 button: PointerButton::Right,
473 },
474 "Alt+Mouse:Right",
475 ),
476 (
477 Binding::ButtonChord {
478 modifier: Modifier::Control,
479 button: PointerButton::Other(9),
480 },
481 "Control+Mouse:9",
482 ),
483 (Binding::PadButton(PadButton::South), "Pad:South"),
484 (
485 Binding::PadChord {
486 modifier: PadButton::LeftShoulder,
487 button: PadButton::South,
488 },
489 "Pad:LeftShoulder+South",
490 ),
491 (
492 Binding::PadTrigger {
493 trigger: Trigger::Right,
494 threshold: 0.25,
495 },
496 "PadTrigger:Right>0.25",
497 ),
498 (
499 Binding::PadStick {
500 stick: Stick::Left,
501 deadzone: 0.2,
502 },
503 "PadStick:Left>0.2",
504 ),
505 (
506 Binding::Wasd {
507 up: KeyCode::KeyW,
508 down: KeyCode::KeyS,
509 left: KeyCode::KeyA,
510 right: KeyCode::KeyD,
511 },
512 "Wasd:KeyW,KeyS,KeyA,KeyD",
513 ),
514 ] {
515 assert_eq!(binding.to_string(), text);
516 }
517 }
518
519 #[test]
520 fn a_name_this_build_does_not_know_is_an_error_naming_the_text() {
521 for text in [
522 "",
523 "KeyNotAKey",
524 "Hyper+KeyR",
525 "Alt+KeyNotAKey",
526 "NotAKey+Scroll",
527 "Mouse:Thumb3",
528 "Mouse:-1",
529 "Alt+Mouse:Thumb3",
530 "Hyper+Mouse:Right",
531 "+Mouse:Right",
532 "AltMouse:Right",
533 "Pointer:Z",
534 "KeyAxis:KeyS",
535 "KeyAxis:KeyS,KeyW,KeyA",
536 "Wasd:KeyW,KeyS,KeyA",
537 "Pad:Paddle1",
538 "Pad:LeftShoulder+",
539 "Pad:Paddle1+South",
540 "PadStick:Middle>0.2",
541 "PadStick:Left",
542 "PadTrigger:Right>much",
543 "Joystick:Left",
544 ] {
545 let error = text.parse::<Binding>().expect_err(text);
546 assert_eq!(error.text, text);
547 assert!(error.to_string().contains("is not a binding"), "{error}");
548 }
549 }
550
551 #[test]
552 fn a_level_parses_whatever_rebind_will_refuse() {
553 assert_eq!(
555 "PadTrigger:Left>1.5".parse::<Binding>(),
556 Ok(Binding::PadTrigger {
557 trigger: Trigger::Left,
558 threshold: 1.5
559 })
560 );
561 }
562}