concinnity_core/components/key_binding.rs
1// InputKey-to-action binding schema.
2
3use crate::ecs::asset_id::AssetId;
4use crate::ecs::asset_id::de_opt_asset_ref;
5use alloc::string::String;
6
7/// Maps a keyboard key to an action string.
8///
9/// When the bound key is pressed, the action fires once per press (like a
10/// [HitRegion](#hitregion) click). Bindings only run while the cursor is free:
11/// they're inactive in worlds that capture the cursor for camera control.
12/// While a [TextInput](#textinput) has keyboard focus, bindings are suspended
13/// so typing cannot trigger actions; a [Screen](#screen)'s `toggle_key` stays
14/// live.
15///
16/// The action vocabulary is the same as [HitRegion](#hitregion)'s:
17/// - `"scene:<name>"`: jump to the named [Scene](#scene)
18/// - `"quit"`: stop the application
19/// - `"screen:show:<name>"`: show the named [Screen](#screen), replacing the top of the stack
20/// - `"screen:push:<name>"`: open the named [Screen](#screen) on top of what is showing
21/// - `"screen:hide"`: close the top [Screen](#screen)
22/// - `"screen:toggle:<name>"`: toggle the named [Screen](#screen)
23///
24/// InputKey names are case-sensitive canonical names (e.g. `"Escape"`, `"Space"`,
25/// `"Enter"`).
26///
27/// ```rust
28/// # use concinnity_core::components::KeyBinding;
29/// KeyBinding {
30/// key: "Escape".into(),
31/// action: "screen:toggle:pause_menu".into(),
32/// ..Default::default()
33/// };
34/// ```
35#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
36#[serde(default)]
37pub struct KeyBinding {
38 /// The key name to bind (e.g. `"Escape"`).
39 pub key: String,
40 /// The action to fire when the key is pressed.
41 pub action: String,
42 /// [Screen](#screen) this binding is scoped to: the binding only fires
43 /// while that screen is on top of the stack. Unset, the binding is global.
44 #[serde(deserialize_with = "de_opt_asset_ref")]
45 pub screen: Option<AssetId>,
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn a_binding_with_no_screen_is_global() {
54 let b = KeyBinding::default();
55 assert!(b.key.is_empty());
56 assert!(b.action.is_empty());
57 assert!(b.screen.is_none());
58 }
59
60 #[test]
61 fn a_screen_scoped_binding_parses_and_round_trips_through_postcard() {
62 crate::test_support::install_resolvers();
63 let b: KeyBinding =
64 serde_json::from_str(r#"{"key":"Escape","action":"back","screen":"menu"}"#).unwrap();
65 assert_eq!(b.key, "Escape");
66 assert_eq!(b.action, "back");
67 assert_eq!(b.screen, Some(AssetId(4)));
68
69 let bytes = postcard::to_allocvec(&b).unwrap();
70 let back: KeyBinding = postcard::from_bytes(&bytes).unwrap();
71 assert_eq!(back.key, "Escape");
72 assert_eq!(back.screen, Some(AssetId(4)));
73 }
74}