aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortcmal <me@aria.rip>2024-08-25 17:44:22 +0100
committertcmal <me@aria.rip>2024-08-25 17:44:22 +0100
commitb380ad94647c9bc446d5a76bb16dcc286077275a (patch)
treeb62b99d26474ce8f7c0caeff37f2e3b9ec166f91
parent5ad62fff05064a6a025a9b947ebab05ed7770e6c (diff)
feat(input): virtual input mapping and codegen
-rw-r--r--Cargo.toml5
-rw-r--r--examples/input-codegen/Cargo.toml9
-rw-r--r--examples/input-codegen/src/main.rs86
-rw-r--r--stockton-input-codegen/Cargo.toml15
-rw-r--r--stockton-input-codegen/src/lib.rs352
-rw-r--r--stockton-input/Cargo.toml7
-rw-r--r--stockton-input/src/axis.rs62
-rw-r--r--stockton-input/src/button.rs74
-rw-r--r--stockton-input/src/lib.rs24
-rw-r--r--stockton-input/src/manager.rs50
10 files changed, 683 insertions, 1 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 713c60f..5ed76d5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,10 @@
[workspace]
members = [
"stockton-types",
+ "stockton-input",
+ "stockton-input-codegen",
"stockton-render",
"stockton-levels",
- "examples/render-bsp"
+ "examples/render-bsp",
+ "examples/input-codegen"
] \ No newline at end of file
diff --git a/examples/input-codegen/Cargo.toml b/examples/input-codegen/Cargo.toml
new file mode 100644
index 0000000..3fc010f
--- /dev/null
+++ b/examples/input-codegen/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "input-codegen"
+version = "0.1.0"
+authors = ["Oscar Shrimpton <oscar.shrimpton.personal@gmail.com>"]
+edition = "2018"
+
+[dependencies]
+stockton-input = { path = "../../stockton-input" }
+stockton-input-codegen = { path = "../../stockton-input-codegen" }
diff --git a/examples/input-codegen/src/main.rs b/examples/input-codegen/src/main.rs
new file mode 100644
index 0000000..8835f2e
--- /dev/null
+++ b/examples/input-codegen/src/main.rs
@@ -0,0 +1,86 @@
+#[macro_use]
+extern crate stockton_input_codegen;
+
+use std::collections::BTreeMap;
+use stockton_input::Action;
+use stockton_input::{Axis, Button, InputManager, InputMutation};
+
+#[derive(InputManager, Default, Debug, Clone)]
+struct MovementInputs {
+ #[axis]
+ vertical: Axis,
+
+ #[axis]
+ horizontal: Axis,
+
+ #[button]
+ jump: Button,
+}
+
+const TEST_ACTIONS: [Action; 10] = [
+ Action::KeyPress(1),
+ Action::KeyRelease(1),
+ Action::KeyPress(2),
+ Action::KeyPress(3),
+ Action::KeyRelease(2),
+ Action::KeyRelease(3),
+ Action::KeyPress(4),
+ Action::KeyPress(5),
+ Action::KeyRelease(4),
+ Action::KeyRelease(5),
+];
+
+// For testing, 1 = w 2 = a
+// 3 = s 4 = d
+// 5 = jump
+fn main() {
+ let mut action_schema = BTreeMap::new();
+ action_schema.insert(
+ 1,
+ (MovementInputsFields::Vertical, InputMutation::PositiveAxis),
+ );
+ action_schema.insert(
+ 3,
+ (MovementInputsFields::Vertical, InputMutation::NegativeAxis),
+ );
+ action_schema.insert(
+ 4,
+ (
+ MovementInputsFields::Horizontal,
+ InputMutation::PositiveAxis,
+ ),
+ );
+ action_schema.insert(
+ 2,
+ (
+ MovementInputsFields::Horizontal,
+ InputMutation::NegativeAxis,
+ ),
+ );
+ action_schema.insert(5, (MovementInputsFields::Jump, InputMutation::MapToButton));
+
+ let mut manager = MovementInputsManager::new(action_schema);
+
+ for action in TEST_ACTIONS.iter() {
+ pretty_print_state(&manager.inputs);
+ manager.handle_frame(std::iter::once(action));
+ }
+ pretty_print_state(&manager.inputs);
+}
+
+fn pretty_print_state(inputs: &MovementInputs) {
+ if *inputs.vertical != 0 {
+ print!("vertical = {} ", *inputs.vertical);
+ }
+ if *inputs.horizontal != 0 {
+ print!("horizontal = {} ", *inputs.horizontal);
+ }
+ if inputs.jump.is_down() {
+ if inputs.jump.is_hot {
+ print!("jump!")
+ } else {
+ print!("jump")
+ }
+ }
+ println!();
+}
diff --git a/stockton-input-codegen/Cargo.toml b/stockton-input-codegen/Cargo.toml
new file mode 100644
index 0000000..b0b01ea
--- /dev/null
+++ b/stockton-input-codegen/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "stockton-input-codegen"
+version = "0.1.0"
+authors = ["Oscar Shrimpton <oscar.shrimpton.personal@gmail.com>"]
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+stockton-input = { path = "../stockton-input" }
+syn = { version = "1.0.44", features = ["full"] }
+proc-macro2 = "1.0.24"
+quote = "1.0.7"
+convert_case = "0.4.0" \ No newline at end of file
diff --git a/stockton-input-codegen/src/lib.rs b/stockton-input-codegen/src/lib.rs
new file mode 100644
index 0000000..db4e5da
--- /dev/null
+++ b/stockton-input-codegen/src/lib.rs
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) Oscar Shrimpton 2020
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+//! Macros for working with stockton_input
+
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident};
+
+/// Generate an input manager for the given struct.
+/// Each button in the struct should be decorated with #[button] and each axis with #[axis].
+/// Given struct MovementInputs, this will output struct MovementInputsManager which implements InputManager.
+/// It also creates an enum MovementInputsFields, with values for all the buttons and axes in MovementInputs.
+/// You'll need to pass in an action schema to `MovementInputsManager::new()`, which is a BTreeMap<u8, (MovementInputsFields, InputMutation)>
+/// You can then call `.handle_frame` on MovementInputsManager and then read the inputs from MovementInputsManager.inputs.
+#[proc_macro_derive(InputManager, attributes(button, axis))]
+pub fn derive_inputmanager(input: TokenStream) -> TokenStream {
+ let struct_data = parse_macro_input!(input as DeriveInput);
+
+ let visibility = &struct_data.vis;
+
+ let struct_ident = &struct_data.ident;
+ let manager_ident = format_ident!("{}Manager", struct_data.ident);
+ let fields_enum_ident = format_ident!("{}Fields", struct_data.ident);
+
+ let (buttons, axes) = get_categorised_idents(&struct_data.data);
+ let caps_buttons = capitalise_idents(buttons.clone());
+ let caps_axes = capitalise_idents(axes.clone());
+
+ let fields_enum = gen_fields_enum(&fields_enum_ident, &caps_buttons, &caps_axes);
+ let manager_struct = gen_manager_struct(
+ &manager_ident,
+ &struct_ident,
+ &fields_enum_ident,
+ buttons.len(),
+ );
+ let trait_impl = gen_trait_impl(
+ &manager_ident,
+ &fields_enum_ident,
+ &buttons,
+ &axes,
+ &caps_buttons,
+ &caps_axes,
+ );
+
+ let expanded = quote! {
+ #[derive(Debug, Clone, Copy)]
+ #visibility #fields_enum
+
+ #[derive(Debug, Clone)]
+ #visibility #manager_struct
+
+ #trait_impl
+
+ };
+
+ TokenStream::from(expanded)
+}
+
+/// Gets the buttons and axes from a given struct definition
+/// Buttons are decorated with #[button] and axes with #[axis]
+fn get_categorised_idents(data: &Data) -> (Vec<Ident>, Vec<Ident>) {
+ let mut buttons = vec![];
+ let mut axes = vec![];
+
+ match data {
+ Data::Struct(ref s) => match &s.fields {
+ Fields::Named(fields) => {
+ for field in fields.named.iter() {
+ let attrs = field.attrs.iter().map(|a| a.parse_meta().unwrap());
+ for attr in attrs {
+ if attr.path().is_ident("button") {
+ buttons.push(field.ident.as_ref().unwrap().clone());
+ break;
+ } else if attr.path().is_ident("axis") {
+ axes.push(field.ident.as_ref().unwrap().clone());
+ break;
+ }
+ }
+ }
+ }
+ _ => unimplemented!(),
+ },
+ _ => {
+ panic!("this is not a struct");
+ }
+ };
+
+ (buttons, axes)
+}
+
+/// Convert a vector of idents to UpperCamel, as used in enums.
+fn capitalise_idents(idents: Vec<Ident>) -> Vec<Ident> {
+ idents
+ .into_iter()
+ .map(capitalise_ident)
+ .collect::<Vec<Ident>>()
+}
+
+/// Convert a single ident to UpperCamel, as used in enums.
+fn capitalise_ident(ident: Ident) -> Ident {
+ format_ident!("{}", ident.to_string().to_case(Case::UpperCamel))
+}
+
+/// Generate an enum for the different buttons and axes in a struct.
+///
+/// Example output:
+/// ```ignore
+/// enum MovementInputsFields {
+/// Jump,
+/// Vertical,
+/// Horizontal,
+/// }
+/// ```
+fn gen_fields_enum(
+ fields_enum_ident: &Ident,
+ buttons_caps: &[Ident],
+ axes_caps: &[Ident],
+) -> TokenStream2 {
+ quote!(
+ enum #fields_enum_ident {
+ #(#buttons_caps,)*
+ #(#axes_caps,)*
+ }
+ )
+}
+
+/// Generates a manager struct for the given inputs struct with buttons_len buttons.
+///
+/// Example output:
+/// ```ignore
+/// struct MovementInputsManager {
+/// inputs: MovementInputs,
+/// actions: BTreeMap<Keycode, ActionResponse>,
+/// just_hot: [bool; 1]
+/// }
+///
+/// impl MovementInputsManager {
+/// pub fn new(actions: BTreeMap<Keycode, ActionResponse>) -> Self {
+/// MovementInputsManager {
+/// inputs: MovementInputs {
+/// vertical: Axis::zero(),
+/// horizontal: Axis::zero(),
+/// jump: Button::new()
+/// },
+/// actions,
+/// just_hot: [false]
+/// }
+/// }
+/// }
+/// ```
+fn gen_manager_struct(
+ ident: &Ident,
+ struct_ident: &Ident,
+ fields_enum_ident: &Ident,
+ buttons_len: usize,
+) -> TokenStream2 {
+ let jh_falses = (0..buttons_len).map(|_| quote!(false));
+ quote!(
+ struct #ident {
+ inputs: #struct_ident,
+ actions: ::std::collections::BTreeMap<u8, (#fields_enum_ident, ::stockton_input::InputMutation)>,
+ just_hot: [bool; #buttons_len]
+ }
+
+ impl #ident {
+ pub fn new(actions: ::std::collections::BTreeMap<u8, (#fields_enum_ident, ::stockton_input::InputMutation)>) -> Self {
+ #ident {
+ inputs: Default::default(),
+ actions,
+ just_hot: [#(#jh_falses),*]
+ }
+ }
+ }
+ )
+}
+
+/// Implements the InputManager trait on a manager struct generated by gen_manager_struct.
+///
+/// Example output:
+/// ```ignore
+/// impl InputManager<Action> for MovementInputsManager {
+/// fn handle_frame<X: IntoIterator<Item = Action>>(&mut self, actions: X) -> () {
+/// // Set just hots back
+/// if self.just_hot[0] {
+/// self.inputs.jump.set_not_hot();
+/// self.just_hot[0] = false;
+/// }
+///
+/// // Deal with actions
+/// for action in actions {
+/// let mutation = self.actions.get(&action.keycode());
+///
+/// if let Some((field, mutation)) = mutation {
+/// let mut val = match mutation {
+/// InputMutation::MapToButton | InputMutation::PositiveAxis => 1,
+/// InputMutation::NegativeAxis => -1
+/// };
+/// if !action.is_down() {
+/// val *= -1
+/// }
+///
+/// match field {
+/// MovementInputsFields::Jump => {
+/// self.inputs.jump.modify_inputs(val > 0);
+/// self.just_hot[0] = true;
+/// },
+/// MovementInputsFields::Vertical => {
+/// self.inputs.vertical.modify(val);
+/// },
+/// MovementInputsFields::Horizontal => {
+/// self.inputs.horizontal.modify(val);
+/// }
+/// }
+/// }
+/// }
+/// }
+/// }
+/// ```
+fn gen_trait_impl(
+ manager: &Ident,
+ fields_enum: &Ident,
+ buttons: &[Ident],
+ axes: &[Ident],
+ buttons_caps: &[Ident],
+ axes_caps: &[Ident],
+) -> TokenStream2 {
+ let just_hot_resets = gen_just_hot_resets(&buttons);
+ let field_match_modify =
+ gen_field_mutation(&buttons, &axes, &buttons_caps, &axes_caps, &fields_enum);
+
+ quote!(
+ impl InputManager for #manager {
+ fn handle_frame<'a, X: IntoIterator<Item = &'a ::stockton_input::Action>>(&mut self, actions: X) -> () {
+ #(#just_hot_resets)*
+
+ for action in actions {
+ let mutation = self.actions.get(&action.keycode());
+
+ if let Some((field, mutation)) = mutation {
+ let mut val = match mutation {
+ InputMutation::MapToButton | InputMutation::PositiveAxis => 1,
+ InputMutation::NegativeAxis => -1
+ };
+ if !action.is_down() {
+ val *= -1
+ }
+
+ #field_match_modify
+ }
+ }
+ }
+ }
+ )
+}
+
+/// Generate the if statements used to reset self.just_hot at the start of each frame
+/// Used by gen_trait_impl.
+fn gen_just_hot_resets(buttons: &[Ident]) -> Vec<TokenStream2> {
+ buttons
+ .iter()
+ .enumerate()
+ .map(|(i, v)| {
+ quote!(
+ if self.just_hot[#i] {
+ self.inputs.#v.set_not_hot();
+ self.just_hot[#i] = false;
+ }
+ )
+ })
+ .collect()
+}
+
+/// Generate the code that actually mutates an input field by matching on a fields enum.
+/// Used by gen_trait_impl.
+fn gen_field_mutation(
+ buttons: &[Ident],
+ axes: &[Ident],
+ buttons_caps: &[Ident],
+ axes_caps: &[Ident],
+ fields_enum_ident: &Ident,
+) -> TokenStream2 {
+ let arms = {
+ let mut btn_arms: Vec<TokenStream2> =
+ gen_mutate_match_arms_buttons(buttons, &buttons_caps, fields_enum_ident);
+ let mut axes_arms = gen_mutate_match_arms_axes(axes, &axes_caps, fields_enum_ident);
+
+ btn_arms.append(&mut axes_arms);
+
+ btn_arms
+ };
+
+ quote!(
+ match field {
+ #(#arms),*
+ };
+ )
+}
+
+/// Used by gen_field_mutation.
+fn gen_mutate_match_arms_buttons(
+ buttons: &[Ident],
+ buttons_caps: &[Ident],
+ fields_enum_ident: &Ident,
+) -> Vec<TokenStream2> {
+ buttons
+ .iter()
+ .enumerate()
+ .zip(buttons_caps.iter())
+ .map(|((idx, field), cap)| {
+ quote!(
+ #fields_enum_ident::#cap => {
+ self.inputs.#field.modify_inputs(val > 0);
+ self.just_hot[#idx] = true;
+ }
+ )
+ })
+ .collect::<Vec<TokenStream2>>()
+}
+
+/// Used by gen_field_mutation.
+fn gen_mutate_match_arms_axes(
+ axes: &[Ident],
+ axes_caps: &[Ident],
+ fields_enum_ident: &Ident,
+) -> Vec<TokenStream2> {
+ axes.iter()
+ .zip(axes_caps.iter())
+ .map(|(field, cap)| {
+ quote!(
+ #fields_enum_ident::#cap => {
+ self.inputs.#field.modify(val);
+ }
+ )
+ })
+ .collect::<Vec<TokenStream2>>()
+}
diff --git a/stockton-input/Cargo.toml b/stockton-input/Cargo.toml
new file mode 100644
index 0000000..39c0666
--- /dev/null
+++ b/stockton-input/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "stockton-input"
+version = "0.1.0"
+authors = ["Oscar Shrimpton <oscar.shrimpton.personal@gmail.com>"]
+edition = "2018"
+
+[dependencies] \ No newline at end of file
diff --git a/stockton-input/src/axis.rs b/stockton-input/src/axis.rs
new file mode 100644
index 0000000..ebf52bb
--- /dev/null
+++ b/stockton-input/src/axis.rs
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) Oscar Shrimpton 2020
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use std::fmt::Debug;
+use std::ops::{Deref, DerefMut};
+
+#[derive(Debug, Clone)]
+/// A linear axis, usually with a value from -1 to 1.
+pub struct Axis(i8);
+
+impl Axis {
+ /// Get a new instance with the value set to zero
+ pub fn zero() -> Self {
+ Axis(0)
+ }
+
+ /// Get the normalized value, ie always positive.
+ pub fn normalized(&self) -> i8 {
+ if self.0 < 0 {
+ -self.0
+ } else {
+ self.0
+ }
+ }
+
+ pub fn modify(&mut self, val: i8) {
+ self.0 += val
+ }
+}
+
+impl Default for Axis {
+ fn default() -> Self {
+ Self::zero()
+ }
+}
+
+impl Deref for Axis {
+ type Target = i8;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for Axis {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
diff --git a/stockton-input/src/button.rs b/stockton-input/src/button.rs
new file mode 100644
index 0000000..9fd5e84
--- /dev/null
+++ b/stockton-input/src/button.rs
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) Oscar Shrimpton 2020
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+use std::fmt::Debug;
+
+#[derive(Debug, Clone, PartialEq)]
+/// A boolean input, with additional tracking for if it just changed state.
+pub struct Button {
+ /// How many of the mapped inputs are currently pressed.
+ /// This is used so that holding one button, then another, then releasing the first, will keep the button down continuously as expected.
+ inputs_down: u8,
+
+ /// Whether or not the button changed state in the last batch of actions processed
+ /// Note that pushing 2 buttons bound to this action one after the other won't trigger this twice.
+ pub is_hot: bool,
+}
+
+impl Button {
+ pub fn new() -> Self {
+ Button {
+ inputs_down: 0,
+ is_hot: false,
+ }
+ }
+
+ pub fn is_down(&self) -> bool {
+ self.inputs_down > 0
+ }
+ pub fn is_up(&self) -> bool {
+ self.inputs_down == 0
+ }
+
+ pub fn is_just_down(&self) -> bool {
+ self.is_down() && self.is_hot
+ }
+ pub fn is_just_up(&self) -> bool {
+ self.is_up() && self.is_hot
+ }
+
+ pub fn modify_inputs(&mut self, add: bool) {
+ self.inputs_down = if add {
+ self.inputs_down + 1
+ } else {
+ self.inputs_down - 1
+ };
+
+ if self.inputs_down == 1 || self.inputs_down == 0 {
+ self.is_hot = true;
+ }
+ }
+
+ pub fn set_not_hot(&mut self) {
+ self.is_hot = false;
+ }
+}
+
+impl Default for Button {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/stockton-input/src/lib.rs b/stockton-input/src/lib.rs
new file mode 100644
index 0000000..7bb779f
--- /dev/null
+++ b/stockton-input/src/lib.rs
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) Oscar Shrimpton 2020
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+pub mod axis;
+pub mod button;
+pub mod manager;
+
+pub use axis::Axis;
+pub use button::Button;
+pub use manager::*;
diff --git a/stockton-input/src/manager.rs b/stockton-input/src/manager.rs
new file mode 100644
index 0000000..2b62a93
--- /dev/null
+++ b/stockton-input/src/manager.rs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) Oscar Shrimpton 2020
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/// A thing that pressing a button can do to an input.
+#[derive(Debug, Clone, Copy)]
+pub enum InputMutation {
+ MapToButton,
+ NegativeAxis,
+ PositiveAxis,
+}
+
+/// A key being pressed or released
+#[derive(Debug, Clone)]
+pub enum Action {
+ KeyPress(u8),
+ KeyRelease(u8),
+}
+
+impl Action {
+ pub fn keycode(&self) -> u8 {
+ match self {
+ Action::KeyPress(x) => *x,
+ Action::KeyRelease(x) => *x,
+ }
+ }
+ pub fn is_down(&self) -> bool {
+ match self {
+ Action::KeyPress(_) => true,
+ Action::KeyRelease(_) => false,
+ }
+ }
+}
+
+pub trait InputManager {
+ fn handle_frame<'a, X: IntoIterator<Item = &'a Action>>(&mut self, actions: X);
+}