Repository: Leafwing-Studios/leafwing-input-manager (rust)

Issue:
Update the `Actionlike` derive macro to easily specify the types
## What problem does this solve?

Currently if the actions are not all Buttons, we need to implement ActionLike manually:
```
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)]
pub enum PlayerActions {
    Up,
    Down,
    Left,
    Right,
    Shoot,
    MoveCursor,
}

impl Actionlike for PlayerActions {
    // Record what kind of inputs make sense for each action.
    fn input_control_kind(&self) -> InputControlKind {
        match self {
            Self::MoveCursor => InputControlKind::DualAxis,
            _ => InputControlKind::Button,
        }
    }
}
```



## What solution would you like?
Have a derive macro to help out:

```
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)]
pub enum PlayerActions {
    Up,
    Down,
    Left,
    Right,
    Shoot,
    #[actionlike(DualAxis)]
    MoveCursor,
}
```


Pull request description:
Add `#[actionlike]` for actions to set their `InputControlKind`s
- Fixes #587

# Solution

Added an `#[actionlike]` attribute to enhance ergonomics.

You can apply this attribute to the entire enum to set a default behavior for all action variants, or to individual variants to override the default behavior.

By default, all actions are treated as button-like, as they were previously.

```rust
#[derive(Actionlike, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)]
#[actionlike(Axis)] // All variants in this enum are controlled by axes
enum CameraAction {
    Zoom,  // This action is controlled by axes
    #[actionlike(DualAxis)]
    Move,  // This action is controlled by dual axes since we have overrode the default option
    #[actionlike(Button)]
    TakePhoto, // This action is controlled by buttons since we have override the default option
}
```

Patch:
diff --git a/RELEASES.md b/RELEASES.md
index 0cf9c3c..5bb32c2 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -8,6 +8,10 @@
 
 - Reflect `Component` and `Resource`, which enables accessing the data in the type registry
 
+#### Actionlike
+
+ - added `#[actionlike]` for actions to set their input kinds, either on an enum or on its individual variants.
+
 #### ActionState
 
 - Reflect `Component` and `Resource`, which enables accessing the data in the type registry
diff --git a/examples/axis_inputs.rs b/examples/axis_inputs.rs
index 3a15f91..e04764b 100644
--- a/examples/axis_inputs.rs
+++ b/examples/axis_inputs.rs
@@ -14,23 +14,15 @@ fn main() {
         .run();
 }
 
-#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
 enum Action {
+    #[actionlike(DualAxis)]
     Move,
     Throttle,
+    #[actionlike(Axis)]
     Rudder,
 }
 
-impl Actionlike for Action {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            Action::Move => InputControlKind::DualAxis,
-            Action::Throttle => InputControlKind::Button,
-            Action::Rudder => InputControlKind::Axis,
-        }
-    }
-}
-
 #[derive(Component)]
 struct Player;
 
diff --git a/examples/default_controls.rs b/examples/default_controls.rs
index 9cdeb73..a1f3874 100644
--- a/examples/default_controls.rs
+++ b/examples/default_controls.rs
@@ -12,22 +12,14 @@ fn main() {
         .run();
 }
 
-#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
 enum PlayerAction {
+    #[actionlike(DualAxis)]
     Run,
     Jump,
     UseItem,
 }
 
-impl Actionlike for PlayerAction {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            PlayerAction::Run => InputControlKind::DualAxis,
-            _ => InputControlKind::Button,
-        }
-    }
-}
-
 impl PlayerAction {
     /// Define the default bindings to the input
     fn default_input_map() -> InputMap<Self> {
diff --git a/examples/input_processing.rs b/examples/input_processing.rs
index 108fe8c..336dc1a 100644
--- a/examples/input_processing.rs
+++ b/examples/input_processing.rs
@@ -10,21 +10,13 @@ fn main() {
         .run();
 }
 
-#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[actionlike(DualAxis)]
 enum Action {
     Move,
     LookAround,
 }
 
-impl Actionlike for Action {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            Action::Move => InputControlKind::DualAxis,
-            Action::LookAround => InputControlKind::DualAxis,
-        }
-    }
-}
-
 #[derive(Component)]
 struct Player;
 
diff --git a/examples/mouse_motion.rs b/examples/mouse_motion.rs
index 1c92557..af7c9ba 100644
--- a/examples/mouse_motion.rs
+++ b/examples/mouse_motion.rs
@@ -10,17 +10,12 @@ fn main() {
         .run();
 }
 
-#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
+#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
+#[actionlike(DualAxis)]
 enum CameraMovement {
     Pan,
 }
 
-impl Actionlike for CameraMovement {
-    fn input_control_kind(&self) -> InputControlKind {
-        InputControlKind::DualAxis
-    }
-}
-
 fn setup(mut commands: Commands) {
     // This will capture the total continuous value, for direct use.
     // Note that you can also use discrete gesture-like motion,
diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs
index 31371ed..9d45707 100644
--- a/examples/mouse_wheel.rs
+++ b/examples/mouse_wheel.rs
@@ -11,24 +11,16 @@ fn main() {
         .run();
 }
 
-#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
+#[derive(Actionlike, Clone, Debug, Copy, PartialEq, Eq, Hash, Reflect)]
 enum CameraMovement {
+    #[actionlike(Axis)]
     Zoom,
+    #[actionlike(DualAxis)]
     Pan,
     PanLeft,
     PanRight,
 }
 
-impl Actionlike for CameraMovement {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            CameraMovement::Zoom => InputControlKind::Axis,
-            CameraMovement::Pan => InputControlKind::DualAxis,
-            CameraMovement::PanLeft | CameraMovement::PanRight => InputControlKind::Button,
-        }
-    }
-}
-
 fn setup(mut commands: Commands) {
     let input_map = InputMap::default()
         // This will capture the total continuous value, for direct use.
diff --git a/examples/twin_stick_controller.rs b/examples/twin_stick_controller.rs
index 5522043..b90824e 100644
--- a/examples/twin_stick_controller.rs
+++ b/examples/twin_stick_controller.rs
@@ -31,22 +31,15 @@ fn main() {
 }
 
 // ----------------------------- Player Action Input Handling -----------------------------
-#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[actionlike(DualAxis)]
 pub enum PlayerAction {
     Move,
     Look,
+    #[actionlike(Button)]
     Shoot,
 }
 
-impl Actionlike for PlayerAction {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            PlayerAction::Move | PlayerAction::Look => InputControlKind::DualAxis,
-            PlayerAction::Shoot => InputControlKind::Button,
-        }
-    }
-}
-
 impl PlayerAction {
     /// Define the default bindings to the input
     fn default_input_map() -> InputMap<Self> {
diff --git a/examples/virtual_dpad.rs b/examples/virtual_dpad.rs
index f2b79f5..7479109 100644
--- a/examples/virtual_dpad.rs
+++ b/examples/virtual_dpad.rs
@@ -14,22 +14,12 @@ fn main() {
         .run();
 }
 
-#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
+#[actionlike(DualAxis)]
 enum Action {
     Move,
 }
 
-impl Actionlike for Action {
-    fn input_control_kind(&self) -> InputControlKind {
-        // We're using a match statement here
-        // because in larger projects, you will likely have
-        // different input control kinds for different actions
-        match self {
-            Action::Move => InputControlKind::DualAxis,
-        }
-    }
-}
-
 #[derive(Component)]
 struct Player;
 
diff --git a/macros/src/actionlike.rs b/macros/src/actionlike.rs
index 6a910c2..8d903e7 100644
--- a/macros/src/actionlike.rs
+++ b/macros/src/actionlike.rs
@@ -1,24 +1,99 @@
 use crate::utils;
-use proc_macro2::TokenStream;
+use proc_macro2::{Span, TokenStream};
 use quote::quote;
-use syn::DeriveInput;
+use std::collections::HashMap;
+use syn::{Attribute, Data, DataEnum, DeriveInput, Error, Ident};
 
 /// This approach and implementation is inspired by the `strum` crate,
 /// Copyright (c) 2019 Peter Glotfelty
 /// available under the MIT License at <https://github.com/Peternator7/strum>
 
-pub(crate) fn actionlike_inner(ast: &DeriveInput) -> TokenStream {
+pub(crate) fn actionlike_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
     // Splitting the abstract syntax tree
     let enum_name = &ast.ident;
     let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
 
     let crate_path = utils::crate_path();
 
-    quote! {
+    let default_control = parse_default_control(ast)?;
+    let input_control_kind_body =
+        generate_input_control_kind_body(ast, &crate_path, &default_control)?;
+    Ok(quote! {
         impl #impl_generics #crate_path::Actionlike for #enum_name #type_generics #where_clause {
             fn input_control_kind(&self) -> #crate_path::InputControlKind {
-                    #crate_path::InputControlKind::Button
+                #input_control_kind_body
+            }
+        }
+    })
+}
+
+fn parse_default_control(ast: &DeriveInput) -> syn::Result<Ident> {
+    if let Some(attr) = ast
+        .attrs
+        .iter()
+        .find(|attr| attr.meta.path().is_ident("actionlike"))
+    {
+        parse_control_attr(attr)
+    } else {
+        Ok(Ident::new("Button", Span::call_site()))
+    }
+}
+
+fn parse_control_attr(attr: &Attribute) -> syn::Result<Ident> {
+    attr.meta
+        .require_list()
+        .and_then(|list| list.parse_args::<Ident>())
+        .map_err(|_| {
+            let span = quote!(#attr);
+            let msg = "expected only one item like `#[actionlike(Button)]`";
+            Error::new_spanned(span, msg)
+        })
+}
+
+fn generate_input_control_kind_body(
+    ast: &DeriveInput,
+    crate_path: &TokenStream,
+    default_control: &Ident,
+) -> Result<TokenStream, Error> {
+    match &ast.data {
+        Data::Enum(enum_data) => {
+            // Gather variants whose control kinds deviate from the default.
+            let controls = parse_variant_controls(enum_data, default_control)?;
+            if controls.is_empty() {
+                return Ok(quote!(#crate_path::InputControlKind::#default_control));
+            }
+
+            let controls: Vec<_> = controls
+                .iter()
+                .map(|(variant, control)| quote!(Self::#variant => #crate_path::InputControlKind::#control,))
+                .collect();
+            Ok(quote! {
+                match self {
+                    #(#controls)*
+                    _ => #crate_path::InputControlKind::#default_control,
+                }
+            })
+        }
+        _ => Ok(quote!(#crate_path::InputControlKind::#default_control)),
+    }
+}
+
+fn parse_variant_controls(
+    data: &DataEnum,
+    default_control: &Ident,
+) -> syn::Result<HashMap<Ident, Ident>> {
+    let mut map = HashMap::<Ident, Ident>::new();
+    for variant in data.variants.iter() {
+        for attr in variant
+            .attrs
+            .iter()
+            .filter(|attr| attr.meta.path().is_ident("actionlike"))
+        {
+            let control = parse_control_attr(attr)?;
+            if &control != default_control {
+                map.insert(variant.ident.clone(), control);
             }
         }
     }
+    Ok(map)
 }
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 39dff71..470c4ea 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -14,16 +14,20 @@ mod typetag;
 
 mod utils;
 
-#[proc_macro_derive(Actionlike)]
+#[proc_macro_derive(Actionlike, attributes(actionlike))]
 pub fn actionlike(input: TokenStream) -> TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
-    crate::actionlike::actionlike_inner(&ast).into()
+    crate::actionlike::actionlike_inner(&ast)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
 }
 
 #[proc_macro_attribute]
 pub fn serde_typetag(_: TokenStream, input: TokenStream) -> TokenStream {
     let ast = syn::parse_macro_input!(input as ItemImpl);
 
-    crate::typetag::expand_serde_typetag(&ast).into()
+    crate::typetag::expand_serde_typetag(&ast)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
 }
diff --git a/macros/src/typetag.rs b/macros/src/typetag.rs
index c495f66..876db47 100644
--- a/macros/src/typetag.rs
+++ b/macros/src/typetag.rs
@@ -8,13 +8,13 @@ use crate::utils;
 /// Copyright (c) 2019 David Tolnay
 /// available under either of `Apache License, Version 2.0` or `MIT` license
 /// at <https://github.com/dtolnay/typetag>
-pub(crate) fn expand_serde_typetag(input: &ItemImpl) -> TokenStream {
+pub(crate) fn expand_serde_typetag(input: &ItemImpl) -> syn::Result<TokenStream> {
     let Some(trait_) = &input.trait_ else {
         let impl_token = input.impl_token;
         let ty = &input.self_ty;
         let span = quote!(#impl_token, #ty);
         let msg = "expected impl Trait for Type";
-        return Error::new_spanned(span, msg).to_compile_error();
+        return Err(Error::new_spanned(span, msg));
     };
 
     let trait_path = &trait_.1;
@@ -24,19 +24,16 @@ pub(crate) fn expand_serde_typetag(input: &ItemImpl) -> TokenStream {
 
     let self_ty = &input.self_ty;
 
-    let ident = match type_name(self_ty) {
-        Some(name) => quote!(#name),
-        None => {
-            let impl_token = input.impl_token;
-            let span = quote!(#impl_token, #self_ty);
-            let msg = "expected explicit name for Type";
-            return Error::new_spanned(span, msg).to_compile_error();
-        }
+    let Some(ident) = type_name(self_ty) else {
+        let impl_token = input.impl_token;
+        let span = quote!(#impl_token, #self_ty);
+        let msg = "expected explicit name for Type";
+        return Err(Error::new_spanned(span, msg));
     };
 
     let crate_path = utils::crate_path();
 
-    quote! {
+    Ok(quote! {
         #input
 
         impl<'de, #generics_params> #crate_path::typetag::RegisterTypeTag<'de, dyn #trait_path> for #self_ty #where_clause {
@@ -52,7 +49,7 @@ pub(crate) fn expand_serde_typetag(input: &ItemImpl) -> TokenStream {
                 )
             }
         }
-    }
+    })
 }
 
 fn type_name(mut ty: &Type) -> Option<String> {
@@ -62,7 +59,7 @@ fn type_name(mut ty: &Type) -> Option<String> {
                 ty = &group.elem;
             }
             Type::Path(TypePath { qself, path }) if qself.is_none() => {
-                return Some(path.segments.last().unwrap().ident.to_string())
+                return Some(path.segments.last()?.ident.to_string())
             }
             _ => return None,
         }
diff --git a/src/action_diff.rs b/src/action_diff.rs
index 6e2f151..fad09fc 100644
--- a/src/action_diff.rs
+++ b/src/action_diff.rs
@@ -339,28 +339,20 @@ impl<A: Actionlike> Default for SummarizedActionState<A> {
 
 #[cfg(test)]
 mod tests {
-    use crate::InputControlKind;
+    use crate as leafwing_input_manager;
 
     use super::*;
     use bevy::{ecs::system::SystemState, prelude::*};
 
-    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
+    #[derive(Actionlike, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
     enum TestAction {
         Button,
+        #[actionlike(Axis)]
         Axis,
+        #[actionlike(DualAxis)]
         DualAxis,
     }
 
-    impl Actionlike for TestAction {
-        fn input_control_kind(&self) -> InputControlKind {
-            match self {
-                TestAction::Button => InputControlKind::Button,
-                TestAction::Axis => InputControlKind::Axis,
-                TestAction::DualAxis => InputControlKind::DualAxis,
-            }
-        }
-    }
-
     fn test_action_state() -> ActionState<TestAction> {
         let mut action_state = ActionState::default();
         action_state.press(&TestAction::Button);
diff --git a/src/clashing_inputs.rs b/src/clashing_inputs.rs
index c98a759..09a94ed 100644
--- a/src/clashing_inputs.rs
+++ b/src/clashing_inputs.rs
@@ -411,7 +411,9 @@ mod tests {
     use crate::prelude::{KeyboardVirtualDPad, UserInput};
     use crate::user_input::ButtonlikeChord;
 
-    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)]
+    use crate as leafwing_input_manager;
+
+    #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)]
     enum Action {
         One,
         Two,
@@ -421,17 +423,9 @@ mod tests {
         CtrlOne,
         AltOne,
         CtrlAltOne,
-        MoveDPad,
         CtrlUp,
-    }
-
-    impl Actionlike for Action {
-        fn input_control_kind(&self) -> crate::InputControlKind {
-            match self {
-                Self::MoveDPad => crate::InputControlKind::DualAxis,
-                _ => crate::InputControlKind::Button,
-            }
-        }
+        #[actionlike(DualAxis)]
+        MoveDPad,
     }
 
     fn test_input_map() -> InputMap<Action> {
diff --git a/src/input_map.rs b/src/input_map.rs
index c45ad9d..35f217c 100644
--- a/src/input_map.rs
+++ b/src/input_map.rs
@@ -61,27 +61,16 @@ fn find_gamepad(_gamepads: &Gamepads) -> Gamepad {
 /// ```rust
 /// use bevy::prelude::*;
 /// use leafwing_input_manager::prelude::*;
-/// use leafwing_input_manager::InputControlKind;
 ///
 /// // Define your actions.
-/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
+/// #[derive(Actionlike, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
 /// enum Action {
+///     #[actionlike(DualAxis)]
 ///     Move,
 ///     Run,
 ///     Jump,
 /// }
 ///
-/// // Because our actions aren't all Buttonlike, we can't derive Actionlike.
-/// impl Actionlike for Action {
-///     // Record what kind of inputs make sense for each action.
-///     fn input_control_kind(&self) -> InputControlKind {
-///         match self {
-///             Action::Move => InputControlKind::DualAxis,
-///             Action::Run | Action::Jump => InputControlKind::Button,
-///         }
-///     }
-/// }
-///
 /// // Create an InputMap from an iterable,
 /// // allowing for multiple input types per action.
 /// let mut input_map = InputMap::new([
@@ -272,14 +261,14 @@ impl<A: Actionlike> InputMap<A> {
     pub fn insert_axis(&mut self, action: A, axis: impl Axislike) -> &mut Self {
         debug_assert!(
             action.input_control_kind() == InputControlKind::Axis,
-            "Cannot map a axislike input for action {:?} of kind {:?}",
+            "Cannot map an axislike input for action {:?} of kind {:?}",
             action,
             action.input_control_kind()
         );
 
         if action.input_control_kind() != InputControlKind::Axis {
             error!(
-                "Cannot map a axislike input for action {:?} of kind {:?}",
+                "Cannot map an axislike input for action {:?} of kind {:?}",
                 action,
                 action.input_control_kind()
             );
@@ -307,14 +296,14 @@ impl<A: Actionlike> InputMap<A> {
     pub fn insert_dual_axis(&mut self, action: A, dual_axis: impl DualAxislike) -> &mut Self {
         debug_assert!(
             action.input_control_kind() == InputControlKind::DualAxis,
-            "Cannot map a axislike input for action {:?} of kind {:?}",
+            "Cannot map an axislike input for action {:?} of kind {:?}",
             action,
             action.input_control_kind()
         );
 
         if action.input_control_kind() != InputControlKind::DualAxis {
             error!(
-                "Cannot map a axislike input for action {:?} of kind {:?}",
+                "Cannot map an axislike input for action {:?} of kind {:?}",
                 action,
                 action.input_control_kind()
             );
diff --git a/src/lib.rs b/src/lib.rs
index 5a42f6b..a6c0280 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,12 +57,8 @@ pub mod prelude {
 /// While `Copy` is not a required trait bound,
 /// users are strongly encouraged to derive `Copy` on these enums whenever possible to improve ergonomics.
 ///
-/// # Warning
-///
-/// The derive macro for this trait assumes that all actions are buttonlike.
-/// If you have axislike or dual-axislike actions, you will need to implement this trait manually.
-///
 /// # Examples
+///
 /// ```rust
 /// use bevy::prelude::Reflect;
 /// use leafwing_input_manager::Actionlike;
@@ -83,30 +79,27 @@ pub mod prelude {
 /// }
 /// ```
 ///
-/// ```rust
-/// use bevy::prelude::Reflect;
-/// use leafwing_input_manager::Actionlike;
-/// use leafwing_input_manager::InputControlKind;
+/// # Customizing variant behavior
 ///
-/// #[derive(PartialEq, Debug, Eq, Clone, Copy, Hash, Reflect)]
-/// enum PlayerAction {
-///    // Movement
-///    Movement,
-///    // Abilities
-///    Ability1,
-///    Ability2,
-///    Ability3,
-///    Ability4,
-///    Ultimate,
-/// }
+/// By default, The derive macro for this trait assumes that all actions are buttonlike.
 ///
-/// impl Actionlike for PlayerAction {
-///     fn input_control_kind(&self) -> InputControlKind {
-///         match self {
-///            PlayerAction::Movement => InputControlKind::DualAxis,
-///            _ => InputControlKind::Button,
-///         }
-///     }
+/// You can customize this behavior by using the `#[actionlike]` attribute,
+/// either on the entire enum or on individual variants.
+///
+/// See the document of [`InputControlKind`] for available options.
+///
+/// ```rust
+/// use bevy::prelude::*;
+/// use leafwing_input_manager::prelude::*;
+///
+/// #[derive(Actionlike, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)]
+/// #[actionlike(Axis)] // This attribute applies to all variants in the enum
+/// enum CameraAction {
+///    Zoom,  // This action is controlled by axes
+///    #[actionlike(DualAxis)]
+///    Move,  // This action is controlled by dual axes since we have overrode the default option
+///    #[actionlike(Button)]
+///    TakePhoto, // This action is controlled by buttons since we have override the default option
 /// }
 /// ```
 pub trait Actionlike:


Test patch:
diff --git a/tests/action_diffs.rs b/tests/action_diffs.rs
index 835ac15..5476431 100644
--- a/tests/action_diffs.rs
+++ b/tests/action_diffs.rs
@@ -2,23 +2,15 @@ use bevy::{input::InputPlugin, prelude::*};
 use leafwing_input_manager::action_diff::{ActionDiff, ActionDiffEvent};
 use leafwing_input_manager::{prelude::*, systems::generate_action_diffs};
 
-#[derive(Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
 enum Action {
     Button,
+    #[actionlike(Axis)]
     Axis,
+    #[actionlike(DualAxis)]
     DualAxis,
 }
 
-impl Actionlike for Action {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            Action::Button => InputControlKind::Button,
-            Action::Axis => InputControlKind::Axis,
-            Action::DualAxis => InputControlKind::DualAxis,
-        }
-    }
-}
-
 fn spawn_test_entity(mut commands: Commands) {
     commands.spawn(ActionState::<Action>::default());
 }
diff --git a/tests/gamepad_axis.rs b/tests/gamepad_axis.rs
index 356a661..4d5de06 100644
--- a/tests/gamepad_axis.rs
+++ b/tests/gamepad_axis.rs
@@ -16,22 +16,15 @@ enum ButtonlikeTestAction {
     Right,
 }
 
-#[derive(Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[actionlike(Axis)]
 enum AxislikeTestAction {
     X,
     Y,
+    #[actionlike(DualAxis)]
     XY,
 }
 
-impl Actionlike for AxislikeTestAction {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            AxislikeTestAction::X | AxislikeTestAction::Y => InputControlKind::Axis,
-            AxislikeTestAction::XY => InputControlKind::DualAxis,
-        }
-    }
-}
-
 fn test_app() -> App {
     let mut app = App::new();
     app.add_plugins(MinimalPlugins)
diff --git a/tests/mouse_motion.rs b/tests/mouse_motion.rs
index 05d82de..dc0d8a0 100644
--- a/tests/mouse_motion.rs
+++ b/tests/mouse_motion.rs
@@ -25,22 +25,15 @@ impl ButtonlikeTestAction {
     }
 }
 
-#[derive(Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[actionlike(Axis)]
 enum AxislikeTestAction {
     X,
     Y,
+    #[actionlike(DualAxis)]
     XY,
 }
 
-impl Actionlike for AxislikeTestAction {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            AxislikeTestAction::X | AxislikeTestAction::Y => InputControlKind::Axis,
-            AxislikeTestAction::XY => InputControlKind::DualAxis,
-        }
-    }
-}
-
 fn test_app() -> App {
     let mut app = App::new();
     app.add_plugins(MinimalPlugins)
diff --git a/tests/mouse_wheel.rs b/tests/mouse_wheel.rs
index 1427f36..5c4d466 100644
--- a/tests/mouse_wheel.rs
+++ b/tests/mouse_wheel.rs
@@ -19,22 +19,15 @@ impl ButtonlikeTestAction {
     }
 }
 
-#[derive(Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+#[actionlike(Axis)]
 enum AxislikeTestAction {
     X,
     Y,
+    #[actionlike(DualAxis)]
     XY,
 }
 
-impl Actionlike for AxislikeTestAction {
-    fn input_control_kind(&self) -> InputControlKind {
-        match self {
-            AxislikeTestAction::X | AxislikeTestAction::Y => InputControlKind::Axis,
-            AxislikeTestAction::XY => InputControlKind::DualAxis,
-        }
-    }
-}
-
 fn test_app() -> App {
     let mut app = App::new();
     app.add_plugins(MinimalPlugins)