Skip to main content

concinnity_asset/
key_binding.rs

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