From 51b7747e62c189d430318c67368a5c84e50ece61 Mon Sep 17 00:00:00 2001 From: Arne Dußin Date: Sun, 17 Jan 2021 13:33:04 +0100 Subject: Input revamp to make keybindings controlable. --- src/button.rs | 183 -------------------------------------------- src/cli/mod.rs | 94 ++++++++++++++--------- src/config.rs | 117 +++++++++++++++++++--------- src/editor.rs | 86 +++++++-------------- src/gui/tool_sidebar.rs | 42 +++++++--- src/input/binding.rs | 123 +++++++++++++++++++++++++++++ src/input/button.rs | 177 ++++++++++++++++++++++++++++++++++++++++++ src/input/mod.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 25 +++--- src/math/rect.rs | 9 ++- src/math/surface.rs | 4 +- src/stable_vec.rs | 107 ++++++++++++++++++++++++++ src/tool/icon_tool.rs | 21 ++--- src/tool/mod.rs | 4 +- 14 files changed, 839 insertions(+), 353 deletions(-) delete mode 100644 src/button.rs create mode 100644 src/input/binding.rs create mode 100644 src/input/button.rs create mode 100644 src/input/mod.rs create mode 100644 src/stable_vec.rs (limited to 'src') diff --git a/src/button.rs b/src/button.rs deleted file mode 100644 index 846377e..0000000 --- a/src/button.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Reimplementation crate of the KeyboardKey and MouseButton structs of raylib, because they do -//! not implement `serde::Serialize` and `serde::Deserialize`. If you have a better idea on how to -//! handle it, feel free. - -use raylib::ffi::{KeyboardKey as rlKeyboardKey, MouseButton as rlMouseButton}; -use raylib::RaylibHandle; -use serde::{Deserialize, Serialize}; -use std::mem; - -/// Enum to abstract the distinction of keyboard keys and mouse keys that raylib does away so the -/// user has free reign over what key they use for what purpose. -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum Button { - /// Button on the mouse with internal mouse button representation of raylib. - Mouse(MouseButton), - /// Keyboard button with internal keyboard key representation of raylib. - Keyboard(KeyboardKey), -} - -#[allow(missing_docs)] -#[repr(u32)] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum MouseButton { - Left = 0, - Right = 1, - Middle = 2, -} - -#[allow(missing_docs)] -#[repr(u32)] -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum KeyboardKey { - Apostrophe = 39, - Comma = 44, - Minus = 45, - Period = 46, - Slash = 47, - Zero = 48, - One = 49, - Two = 50, - Three = 51, - Four = 52, - Five = 53, - Six = 54, - Seven = 55, - Eight = 56, - Nine = 57, - Semicolon = 59, - Equal = 61, - A = 65, - B = 66, - C = 67, - D = 68, - E = 69, - F = 70, - G = 71, - H = 72, - I = 73, - J = 74, - K = 75, - L = 76, - M = 77, - N = 78, - O = 79, - P = 80, - Q = 81, - R = 82, - S = 83, - T = 84, - U = 85, - V = 86, - W = 87, - X = 88, - Y = 89, - Z = 90, - Space = 32, - Escape = 256, - Enter = 257, - Tab = 258, - Backspace = 259, - Insert = 260, - Delete = 261, - Right = 262, - Left = 263, - Down = 264, - Up = 265, - PageUp = 266, - PageDown = 267, - Home = 268, - End = 269, - CapsLock = 280, - ScrollLock = 281, - NumLock = 282, - PrintScreen = 283, - Pause = 284, - F1 = 290, - F2 = 291, - F3 = 292, - F4 = 293, - F5 = 294, - F6 = 295, - F7 = 296, - F8 = 297, - F9 = 298, - F10 = 299, - F11 = 300, - F12 = 301, - LeftShift = 340, - LeftControl = 341, - LeftAlt = 342, - LeftSuper = 343, - RightShift = 344, - RightControl = 345, - RightAlt = 346, - RightSuper = 347, - Menu = 348, - LeftBracket = 91, - Backslash = 92, - RightBracket = 93, - Grave = 96, - Keypad0 = 320, - Keypad1 = 321, - Keypad2 = 322, - Keypad3 = 323, - Keypad4 = 324, - Keypad5 = 325, - Keypad6 = 326, - Keypad7 = 327, - Keypad8 = 328, - Keypad9 = 329, - KeypadDecimal = 330, - KeypadDivide = 331, - KeypadMultiply = 332, - KeypadSubtract = 333, - KeypadAdd = 334, - KeypadEnter = 335, - KeypadEqual = 336, -} - -impl Button { - /// Check if this button is pressed. If `mouse_blocked` is true, mouse buttons are ignored which - /// is useful when an element has captured the mouse, but other elements are still queried in the - /// background. - pub fn is_pressed(self, rl: &RaylibHandle, mouse_blocked: bool) -> bool { - match self { - Self::Mouse(button) => !mouse_blocked && rl.is_mouse_button_pressed(button.into()), - Self::Keyboard(key) => rl.is_key_pressed(key.into()), - } - } -} - -impl From for Button { - fn from(button: rlMouseButton) -> Self { - Self::Mouse(MouseButton::from(button)) - } -} -impl From for Button { - fn from(key: rlKeyboardKey) -> Self { - Self::Keyboard(KeyboardKey::from(key)) - } -} - -impl From for MouseButton { - fn from(button: rlMouseButton) -> Self { - unsafe { mem::transmute(button as u32) } - } -} -impl Into for MouseButton { - fn into(self) -> rlMouseButton { - unsafe { mem::transmute(self as u32) } - } -} - -impl From for KeyboardKey { - fn from(key: rlKeyboardKey) -> Self { - unsafe { mem::transmute(key as u32) } - } -} -impl Into for KeyboardKey { - fn into(self) -> rlKeyboardKey { - unsafe { mem::transmute(self as u32) } - } -} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a654e19..370a30b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -12,81 +12,99 @@ pub mod cmd; pub use self::cmd::*; use crate::colours::DEFAULT_COLOURS; +use crate::input::{Button, Input}; use crate::math::Vec2; use crate::Editor; use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; -use raylib::ffi::KeyboardKey; -use raylib::RaylibHandle; +use std::sync::mpsc::Receiver; /// The command line interface. Should be created only once per program instance. pub struct CLI { text: String, - active: bool, + char_pipe: Option>, } impl CLI { /// Create a CLI for this instance - #[allow(clippy::new_without_default)] - pub fn new() -> Self { + pub fn new(input: &mut Input) -> Self { + input.add_global(Button::Text(':').into()); + Self { text: String::new(), - active: false, + char_pipe: None, } } /// Activates the CLI, which will now capture keyboard input and execute commands accordingly. - pub fn activate(&mut self) { - if !self.active { - self.text = ";".to_owned(); - self.active = true; + pub fn try_activate(&mut self, input: &mut Input) { + if !self.active() { + self.char_pipe = input.try_capture_keyboard(); + + if self.char_pipe.is_some() { + self.text = ":".to_owned(); + } } } + /// Deactivate the command line. + pub fn deactivate(&mut self) { + // Hang up, the input handler will get the message. + self.char_pipe = None; + } + /// Checks if the CLI is currently active. This means input to other things should be ignored. pub fn active(&self) -> bool { - self.active + self.char_pipe.is_some() + } + + fn perform_command(&mut self, editor: &mut Editor) { + match cmd::parse_command(&self.text[1..]) { + Ok(cmd) => match cmd.process(editor) { + Ok(res) => self.text = format!("SUCCESS: {}", res), + Err(err) => self.text = format!("ERROR: {}", err), + }, + Err(err) => self.text = format!("SYNTAX ERROR: {}", err), + } } /// Handle input for the command line and perform any commands the user may want to run. - pub fn update(&mut self, rl: &mut RaylibHandle, editor: &mut Editor) { + pub fn update(&mut self, editor: &mut Editor, input: &mut Input) { /* Check if the CLI is currently active. If not and it should not be activated according to * keyboard input, there is nothing to do. */ - if !self.active { - if rl.is_key_pressed(KeyboardKey::KEY_SEMICOLON) { - // Don't write the keypress again. - rl.get_key_pressed(); - self.activate(); + if !self.active() { + if input.poll_global(&Button::Text(':').into()) { + self.try_activate(input); + if !self.active() { + return; + } } else { return; } } - // The CLI is currently active. Handle input to it. - if let Some(key) = rl.get_key_pressed_number() { - self.text.push(key as u8 as char); - } else if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { - self.text.pop(); - } else if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { - self.text.clear(); + let rx = self + .char_pipe + .as_mut() + .expect("No character pipe eventhough CLI should be active"); + + if let Ok(c) = rx.try_recv() { + match c { + '\x1B' => self.text.clear(), // Escape + '\x7f' => { + self.text.pop(); + } // Backspace + '\n' => { + self.perform_command(editor); + self.deactivate(); + } + c => self.text.push(c), + } } // When the text is empty, there is also no command marker, so set as inactive and leave. if self.text.is_empty() { - self.active = false; - return; - } - - // Perform the entered command, when the enter-key is pressed. - if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { - self.active = false; - match cmd::parse_command(&self.text[1..]) { - Ok(cmd) => match cmd.process(editor) { - Ok(res) => self.text = format!("SUCCESS: {}", res), - Err(err) => self.text = format!("ERROR: {}", err), - }, - Err(err) => self.text = format!("SYNTAX ERROR: {}", err), - } + self.deactivate() } } diff --git a/src/config.rs b/src/config.rs index b5abb1e..6d0680c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ //! Home of the user configuratable content of graf karto, like keybindings and (TODO) colours etc. -use crate::button::*; +use crate::input::{Binding, Button, Input, MouseButton, Scancode}; use ron::de::from_reader; use ron::ser::{to_string_pretty, PrettyConfig}; use serde::{Deserialize, Serialize}; @@ -12,49 +12,49 @@ use std::path::Path; #[allow(missing_docs)] #[derive(Deserialize, Serialize)] pub struct Config { - pub tool_activation_keys: ToolActivationKeys, - pub tool_general_keys: ToolGeneralKeys, - pub icon_keys: IconToolKeys, + pub tool_activation_binds: ToolActivationBinds, + pub tool_general_binds: ToolGeneralBinds, + pub icon_binds: IconToolBinds, } #[allow(missing_docs)] #[derive(Deserialize, Serialize)] /// The keys used to activate the individual tools. These keystrokes will not be sent to the tools, /// but instead will be handled by the editor where the tools are registered. -pub struct ToolActivationKeys { - pub deletion: Button, - pub icon: Button, - pub polygon_room: Button, - pub rect_room: Button, - pub selection: Button, - pub wall: Button, +pub struct ToolActivationBinds { + pub deletion: Binding, + pub icon: Binding, + pub polygon_room: Binding, + pub rect_room: Binding, + pub selection: Binding, + pub wall: Binding, } #[derive(Deserialize, Serialize)] /// Keys that are useful to most tools. These are packaged so that not every tool has the same n keys /// and then some more. -pub struct ToolGeneralKeys { +pub struct ToolGeneralBinds { /// Keybinding to, where applicable, place a single node (usually a vertex) for the tool in /// question. - pub place_single: Button, + pub place_single: Binding, /// Finish up whatever one is doing with the current tool, without removing information. - pub finish: Button, + pub finish: Binding, /// Abort whatever one is doing with the current tool which means the last atomic action will not /// be pushed into the map items. - pub abort: Button, + pub abort: Binding, } #[derive(Clone, Serialize, Deserialize)] /// Key bindings that are individually interesting to the icon tool. -pub struct IconToolKeys { +pub struct IconToolBinds { /// Key to change to the next icon of the icon list. - pub next: Button, + pub next: Binding, /// Key to change to the previous icon of the icon list. - pub previous: Button, + pub previous: Binding, /// Rotate the working icon clockwise by a certain amount (currently 45 degrees) - pub rotate_clockwise: Button, + pub rotate_clockwise: Binding, /// Rotate the working icon counterclockwise by a certain amount (currently 45 degrees) - pub rotate_counterclockwise: Button, + pub rotate_counterclockwise: Binding, } impl Config { @@ -94,27 +94,72 @@ impl Config { } } +/// Registers all bindings from the given configuration into the input handler. Should the +/// configuration change at runtime, the global bindings of the input handler need to be cleared and +/// this function must be called again. +pub fn register_bindings(config: &Config, input: &mut Input) { + if !input.add_global(config.tool_activation_binds.deletion.clone()) { + warn!("Tried to add deletion binding twice."); + } + if !input.add_global(config.tool_activation_binds.icon.clone()) { + warn!("Tried to add icon binding twice."); + } + if !input.add_global(config.tool_activation_binds.polygon_room.clone()) { + warn!("Tried to add polygon room binding twice."); + } + if !input.add_global(config.tool_activation_binds.rect_room.clone()) { + warn!("Tried to add rect room binding twice."); + } + if !input.add_global(config.tool_activation_binds.selection.clone()) { + warn!("Tried to add selection binding twice."); + } + if !input.add_global(config.tool_activation_binds.wall.clone()) { + warn!("Tried to add wall binding twice."); + } + if !input.add_global(config.tool_general_binds.place_single.clone()) { + warn!("Tried to add place single binding twice."); + } + if !input.add_global(config.tool_general_binds.finish.clone()) { + warn!("Tried to add finish binding twice."); + } + if !input.add_global(config.tool_general_binds.abort.clone()) { + warn!("Tried to add abort binding twice."); + } + if !input.add_global(config.icon_binds.next.clone()) { + warn!("Tried to add next binding twice."); + } + if !input.add_global(config.icon_binds.previous.clone()) { + warn!("Tried to add previous binding twice."); + } + if !input.add_global(config.icon_binds.rotate_clockwise.clone()) { + warn!("Tried to add rotate clockwise binding twice."); + } + if !input.add_global(config.icon_binds.rotate_counterclockwise.clone()) { + warn!("Tried to add rotate counterclockwise binding twice."); + } +} + impl Default for Config { fn default() -> Self { Config { - tool_activation_keys: ToolActivationKeys { - deletion: Button::Keyboard(KeyboardKey::D), - icon: Button::Keyboard(KeyboardKey::I), - polygon_room: Button::Keyboard(KeyboardKey::P), - rect_room: Button::Keyboard(KeyboardKey::R), - selection: Button::Keyboard(KeyboardKey::S), - wall: Button::Keyboard(KeyboardKey::W), + tool_activation_binds: ToolActivationBinds { + deletion: Button::Text('d').into(), + icon: Button::Text('i').into(), + polygon_room: Button::Text('p').into(), + rect_room: Button::Text('r').into(), + selection: Button::Text('s').into(), + wall: Button::Text('w').into(), }, - tool_general_keys: ToolGeneralKeys { - place_single: Button::Mouse(MouseButton::Left), - finish: Button::Keyboard(KeyboardKey::Enter), - abort: Button::Mouse(MouseButton::Right), + tool_general_binds: ToolGeneralBinds { + place_single: Button::Mouse(MouseButton::Left).into(), + finish: Button::Scancode(Scancode::Enter).into(), + abort: Button::Mouse(MouseButton::Right).into(), }, - icon_keys: IconToolKeys { - next: Button::Keyboard(KeyboardKey::I), - previous: Button::Keyboard(KeyboardKey::J), - rotate_clockwise: Button::Mouse(MouseButton::Right), - rotate_counterclockwise: Button::Keyboard(KeyboardKey::Minus), + icon_binds: IconToolBinds { + next: Button::Text('j').into(), + previous: Button::Text('k').into(), + rotate_clockwise: Button::Text('+').into(), + rotate_counterclockwise: Button::Text('-').into(), }, } } diff --git a/src/editor.rs b/src/editor.rs index abbc401..7bf8f5e 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -5,8 +5,8 @@ //! currently a difference between things that are being created (inside the editor) and things that //! are part of the environment (the map). -use crate::button::{Button, MouseButton}; use crate::config::Config; +use crate::input::{Binding, Input}; use crate::map::Map; use crate::snapping::Snapper; use crate::tool::*; @@ -20,7 +20,7 @@ pub struct Editor { map: Map, /// HashMap that matches the ToolType with its proper activation key and of course the tool /// itself. - tools: HashMap, Button)>, + tools: HashMap, Binding)>, active: ToolType, config: Config, } @@ -31,46 +31,52 @@ impl Editor { pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread, config: Config) -> Self { let map = Map::new(rl, rlt); - let mut tools: HashMap, Button)> = + let mut tools: HashMap, Binding)> = HashMap::with_capacity(ToolType::NumTools as usize); tools.insert( ToolType::RectRoomTool, ( Box::new(RectRoomTool::new()), - config.tool_activation_keys.rect_room, + config.tool_activation_binds.rect_room.clone(), ), ); tools.insert( ToolType::PolygonRoomTool, ( Box::new(PolygonRoomTool::new()), - config.tool_activation_keys.polygon_room, + config.tool_activation_binds.polygon_room.clone(), ), ); tools.insert( ToolType::WallTool, - (Box::new(WallTool::new()), config.tool_activation_keys.wall), + ( + Box::new(WallTool::new()), + config.tool_activation_binds.wall.clone(), + ), ); tools.insert( ToolType::IconTool, ( - Box::new(IconTool::new(config.icon_keys.clone(), map.icon_renderer())), - config.tool_activation_keys.icon, + Box::new(IconTool::new( + config.icon_binds.clone(), + map.icon_renderer(), + )), + config.tool_activation_binds.icon.clone(), ), ); tools.insert( ToolType::DeletionTool, ( Box::new(DeletionTool::new()), - config.tool_activation_keys.deletion, + config.tool_activation_binds.deletion.clone(), ), ); tools.insert( ToolType::SelectionTool, ( Box::new(SelectionTool::new()), - config.tool_activation_keys.selection, + config.tool_activation_binds.selection.clone(), ), ); @@ -111,22 +117,19 @@ impl Editor { rl: &mut RaylibHandle, transform: &Transform, snapper: &Snapper, - mouse_blocked: bool, - keyboard_captured: bool, + input: &mut Input, ) { // Handle keybindings for tool change - if !keyboard_captured { - for (&tool_type, (_, activation_key)) in self.tools.iter() { - if activation_key.is_pressed(rl, false) { - // Don't do anything if the tool does not change. - if tool_type == self.active { - break; - } - - // Activate the tool of which the key binding has been pressed. - self.set_active(tool_type); + for (&tool_type, (_, activation_bind)) in self.tools.iter() { + if input.poll_global(&activation_bind) { + // Don't do anything if the tool does not change. + if tool_type == self.active { break; } + + // Activate the tool of which the key binding has been pressed. + self.set_active(tool_type); + break; } } @@ -138,49 +141,18 @@ impl Editor { active_tool.update(&self.map, &snapped_mouse_pos); // Handle common keybindings many of the tools have. - if self - .config - .tool_general_keys - .place_single - .is_pressed(rl, mouse_blocked) - { + if input.poll_global(&self.config.tool_general_binds.place_single) { active_tool.place_single(&mut self.map, &snapped_mouse_pos); } - if self - .config - .tool_general_keys - .finish - .is_pressed(rl, mouse_blocked) - { + if input.poll_global(&self.config.tool_general_binds.finish) { active_tool.finish(&mut self.map); } - if self - .config - .tool_general_keys - .abort - .is_pressed(rl, mouse_blocked) - { + if input.poll_global(&self.config.tool_general_binds.abort) { active_tool.abort(); } // Handle custom keybindings in case the tool has any. - let latest_button = if let Some(keyboard_key) = rl.get_key_pressed() { - Some(Button::Keyboard(keyboard_key.into())) - } else { - let mouse_buttons = [ - Button::Mouse(MouseButton::Left), - Button::Mouse(MouseButton::Middle), - Button::Mouse(MouseButton::Right), - ]; - mouse_buttons - .iter() - .find(|button| button.is_pressed(rl, mouse_blocked)) - .copied() - }; - - if let Some(latest_button) = latest_button { - active_tool.on_button_pressed(&mut self.map, latest_button); - } + active_tool.handle_custom_bindings(&mut self.map, input); } /// Draw all tools and in case of the active tool also what is currently being edited by it, if diff --git a/src/gui/tool_sidebar.rs b/src/gui/tool_sidebar.rs index 78041e7..af6af74 100644 --- a/src/gui/tool_sidebar.rs +++ b/src/gui/tool_sidebar.rs @@ -3,7 +3,8 @@ // TODO: Currently, the keyboard shortcuts for tools are handled by the editor, but a lot speaks for // them being handled by the ToolSidebar instead. -use crate::math::{ExactSurface, Rect, Vec2}; +use crate::input::Input; +use crate::math::Rect; use crate::tool::ToolType; use crate::Editor; use raylib::core::texture::Texture2D; @@ -17,37 +18,56 @@ pub const BUTTON_FILE: &str = "assets/button/tool_buttons.png"; /// Sidebar that renders and handles input for the tool activation buttons. pub struct ToolSidebar { button_texture: Texture2D, + bindings_id: usize, + panel_rect: Rect, } impl ToolSidebar { /// Create a new tool sidebar. There should be only one sidebar per program instance. - pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { + pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread, input: &mut Input) -> Self { let button_texture = rl .load_texture(rlt, BUTTON_FILE) .expect("Could not read file containing tool icons."); - Self { button_texture } + let panel_rect = Self::panel_rect(rl.get_screen_height() as u16); + let bindings_id = input.add_local_handler(panel_rect.clone()); + + Self { + button_texture, + bindings_id, + panel_rect, + } } - fn panel_rect(screen_height: u16) -> Rect { + fn panel_rect(screen_height: u16) -> Rect { /* The width is currently hardcoded as 104, which is * 64 (button-size) + 20 left gap + 20 right gap */ - Rect::new(0., 0., 104., screen_height as f32) + Rect::new(0, 0, 104, screen_height) } - /// Check if the mouse is currently being captured by this GUI-element. In that case, - /// everything else that might want to access the mouse will be blocked. - pub fn mouse_captured(screen_height: u16, mouse_pos: Vec2) -> bool { - Self::panel_rect(screen_height).contains_point(&mouse_pos) + /// Update the state of the tool sidebar. Due to raylib limitations, this is not where the tools + /// are selected for the editor, which happens in draw. + pub fn update(&mut self, screen_height: u16, input: &mut Input) { + let new_panel_rect = Self::panel_rect(screen_height); + if new_panel_rect != self.panel_rect { + self.panel_rect = new_panel_rect; + input.set_binding_rect(self.bindings_id, self.panel_rect); + } } /// Draw the tool buttons and encasing panel. Because of the way raylib works, this also handles /// clicking on tool buttons, which may be changed in the future, should a different gui be /// chosen. - pub fn draw(&self, screen_height: u16, rld: &mut impl RaylibDrawGui, editor: &mut Editor) { - rld.gui_panel(Self::panel_rect(screen_height)); + pub fn draw(&self, rld: &mut impl RaylibDrawGui, editor: &mut Editor) { + rld.gui_panel(Rect::new( + self.panel_rect.x as f32, + self.panel_rect.y as f32, + self.panel_rect.w as f32, + self.panel_rect.h as f32, + )); + // TODO: Update to new input system. Create buttons that integrate. let mut active = editor.active(); for i in 0..ToolType::NumTools as usize { let is_current_active = active as usize == i; diff --git a/src/input/binding.rs b/src/input/binding.rs new file mode 100644 index 0000000..386fb66 --- /dev/null +++ b/src/input/binding.rs @@ -0,0 +1,123 @@ +//! Bindings module, which is a key combination that does something when pressed. + +use super::Button; +use raylib::RaylibHandle; +use serde::{Deserialize, Serialize}; + +/// Binding struct, which holds any number of buttons (keyboard and mouse may be mixed, if desired) +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Binding { + buttons: Vec