Open-source project
Leafwing-Studios/leafwing-input-manager avatar
Leafwing-Studios/leafwing-input-manager

leafwing-input-manager: logical actions instead of raw key bindings

A straightforward stateful input manager for the Bevy game engine.

972 stars157 forksRustApache-2.0

At a glance

What is it?
This Bevy plugin collects keyboard, mouse and gamepad input into a per-entity ActionState, with an InputMap component holding many-to-many bindings between inputs and logical actions, chord support, clash resolution and a serializable diff for networked play. Its one documented limitation is that gamepads have to be assigned to a map by hand.
Who is it for?
Adopt leafwing-input-manager when a Bevy project has outgrown ad hoc key bindings, when it needs rebindable controls, local multiplayer on separate entities, or a wire format for input state, since the ActionState and ActionDiff models cover those cases directly. It is the wrong tool if you need gamepads to be detected without any setup, because assignment is manual, and it is a heavy dependency for a project that only reads a couple of keys.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why raw key bindings stop working

The problem this crate addresses is easy to describe and hard to solve well. A game wants to know whether the player is jumping, not whether the space bar is down. The moment you write the second kind of check, the codebase fills up with if input.just_pressed(KeyCode::Space) scattered across systems, and then a second player, a gamepad, a rebinding screen or a replay system makes every one of those checks wrong in a different way.

The plugin's answer is to separate inputs from actions. Inputs from various sources, keyboard, mouse and gamepad, are collected into a common ActionState on your player entity, which is then used in game logic. The mapping between the two lives in an InputMap component, and it is many-to-many in both directions. A single action can be triggered by multiple inputs, or set directly by UI elements or gameplay logic, and a single input can result in multiple actions being triggered, which can be handled contextually.

That many-to-many property is the part that makes the design work rather than being a convenience. The alternative, a struct of key bindings per device, means every new input source multiplies the number of places you have to keep in sync, which the README describes as the headache of bespoke Keybindings<KeyCode> and Keybindings<Gamepad> types. Here there is one map and one state, and the game logic asks about actions.

The plugin is also explicit about what it is not doing to you. The action set is a type you define, an enum with the Actionlike trait derived, so the action names are checked by the compiler and the state you read is strongly typed. The README notes the crate carries Leafwing Studio's trademark forbid(missing_docs), which means every public item in the API has to be documented, and the effect of that policy is visible in the docs.rs listing for the crate.

There is one more structural choice worth flagging. The ActionState is a component, not a global resource, and the README calls out the practical reason: the sixteen system parameter limit. A game that gathered all input into a global resource would have to pass that resource into every system that needed it, and the plugin replaces that with a query.

Version pairing, the crate manifest and what you pull in

The compatibility table in the README is the first thing to read, because this crate is versioned against the Bevy release it supports. Bevy 0.20 pairs with leafwing-input-manager 0.22, Bevy 0.19 with 0.21, Bevy 0.18 with 0.20, Bevy 0.17 with 0.18 and 0.19, Bevy 0.16 with 0.17, and the table continues down to Bevy 0.9 with 0.7 and 0.8. Older Bevy versions are served by older releases of the plugin rather than by a single version that spans everything, which is the normal outcome of a crate that leans on the engine's internals.

The manifest in the repository root gives the current state: version 0.22.0, edition 2024, categories of games and game-development, and the licence field set to MIT OR Apache-2.0, with the two licence texts sitting next to each other as LICENSE-APACHE.md and LICENSE-MIT.md. The workspace has three members, the crate itself, a tools/ci package and a macros package, and the macro crate is a path dependency at version 0.17, which is what provides the derive for Actionlike.

The dependency on Bevy itself is deliberately narrow. The manifest asks for bevy with default-features disabled and only the serialize, std, bevy_log and gamepad features, which keeps the plugin from pulling in the rendering, audio and windowing stack of the full engine. The winit dependency is optional and exists only so that docs.rs can build, behind a docs feature the README says users should never need to enable.

The feature flags are where the real choices live. The default set is asset, mouse, keyboard, gamepad and picking, so a default install already includes mouse, keyboard and gamepad support, asset support for using the InputMap as a bevy::asset::Asset, and integration with bevy::picking so that picking observers can modify action state. The timing feature adds measurement of how long a button was held, and the gamepad feature pulls in bevy/bevy_gilrs, which is where the gamepad handling comes from. A project that only needs keyboard and mouse can switch the gamepad feature off and drop the gilrs dependency with it.

One more manifest detail is worth noting: the dev profile sets opt-level 3, so a development build of the game is optimised rather than unoptimised. That is a deliberate choice for a crate whose users are usually profiling input latency, and it also means debug builds are not as slow as the default profile would suggest.

The minimal setup, from plugin to a jump check

The README's getting started list is six steps: add the crate to Cargo.toml, create an enum of logical actions and derive Actionlike for it, add the InputManagerPlugin to the App, add an InputMap component to the player entity, configure a mapping, and read the ActionState.

The example that follows is the minimal.rs file from the examples directory, and it is short enough to read in full. First the plugin and the systems:

rust
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // This plugin maps inputs to an input-type agnostic action-state
        // We need to provide it with an enum which stores the possible actions a player could take
        .add_plugins(InputManagerPlugin::<Action>::default())
        // The InputMap and ActionState components will be added to any entity with the Player component
        .add_systems(Startup, spawn_player)
        // Read the ActionState in your systems using queries!
        .add_systems(Update, jump)
        .run();
}

Then the action enum, the player marker and the spawn that installs the map:

rust
// This is the list of "things in the game I want to be able to do based on input"
#[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
enum Action {
    Run,
    Jump,
}

#[derive(Component)]
struct Player;

fn spawn_player(mut commands: Commands) {
    // Describes how to convert from player inputs into those actions
    let input_map = InputMap::new([(Action::Jump, KeyCode::Space)]);
    commands.spawn(input_map).insert(Player);
}

And finally the query that reads the state:

rust
// Query for the `ActionState` component in your game logic systems!
fn jump(query: Query<&ActionState<Action>, With<Player>>) {
    let action_state = query.single();
    if let Ok(action_state) = action_state {
        // Each action has a button-like state of its own that you can check
        if action_state.just_pressed(&Action::Jump) {
            println!("I'm jumping!");
        }
    }
}

Two details in that snippet carry most of the value. The ActionState carries per-action state, so just_pressed is a method on the action rather than a global edge detection, which means two actions triggered by the same key do not both claim the press.

The examples directory is where the rest of the API is demonstrated, with files for axis inputs, mouse motion and wheel, virtual D-pads, triggers, press duration, input processing, default controls, clash handling, multiplayer, twin stick controller, arpg indirection, register_gamepads and sending actions over the network.

Chords, clash strategy and the ambiguity of one key

A controller that maps a key to an action has to answer an awkward question: what happens when the player presses a key that is bound to two actions, or holds a combination that includes a chord? The plugin treats this as a first-class problem rather than leaving it to the user.

Arbitrary button combinations are supported, so a console action can be bound to a chord of three keys, and the README shows the exact call for that. The combination is a ButtonlikeChord built from a list of buttons, and the ergonomic insertion API means the same action can be given a keyboard button and a gamepad button in one call, so the choice between input_map.insert(Action::Jump, KeyCode::Space) and the gamepad equivalent is not a decision you have to make.

The clash handling is the more interesting half. The README describes input disambiguation with a ClashStrategy enum, whose purpose is to stop triggering individual buttons when what the player meant was a chord. Without it, pressing Control, Shift and C in sequence can fire the individual actions bound to each of those keys before the chord fires, which in a game means the wrong thing happens. The clash_handling.rs example is the place to look for the strategies in action, and since the strategy is a choice rather than a fixed behaviour, a game that wants different rules for movement keys and menu keys can configure them per map.

Analog input gets the same treatment. There is dual axis support for analog inputs from gamepads and joysticks, with axis processing for deadzones, sensitivity and clamping, and the axis_inputs.rs example shows how a stick is turned into a direction action. The twin stick controller example and the virtual D-pad example cover the two common shapes where a single physical input has to fan out into several logical ones.

Per-entity state, local multiplayer and the wire format

Because the map and the state are components, the plugin supports more than one player without special casing. The README describes local multiplayer support as binding keys to distinct entities rather than worrying about singular global state, and the multiplayer.rs example covers it. The same mechanism gives you the ability to create disjoint action sets, which the README calls strongly typed disjoint action sets, by adding multiple copies of the plugin with different action types, so a camera and a player character can have separate action enums with no overlap.

For networked play, the data is serializable, and there is a space-conscious ActionDiff representation to send on the wire, which the send_actions_over_network.rs example demonstrates. The distinction between the full state and the diff is the difference between sending everything and sending what changed, and in a game with a dozen players on a slow link that difference is the whole budget.

Two more features are aimed at production rather than gameplay. The plugin can control which state it is active in, so a player does not wander around while a menu is open, which the README describes as stopping wandering around while in a menu. And there is an input mocking API for integration testing, so a test can drive an action directly rather than synthesising key events, which is what makes automated tests of a Bevy application practical at all.

The UI side has its own story. Buttons can be wired to game state with one component, and when clicked the button presses the appropriate action on the corresponding entity. The picking feature flag orders systems so that picking observers can modify action state.

The one documented limitation, and the manual gamepad assignment

The README has a Limitations section and it contains a single entry: gamepads must be manually assigned to each input map, with the instruction to read from the Gamepads resource and use InputMap::set_gamepad. Nothing connects a plugged-in controller to a player automatically, and the register_gamepads.rs example exists precisely to show the work involved.

That is a real ergonomic tax. In a console game where a player plugs in a controller and expects the game to notice, the plugin makes you handle the connection event, decide which entity owns that pad, and call set_gamepad. The upside of the manual approach is that it hands the assignment decision to the game, which matters in local multiplayer where you need to decide which pad belongs to which player, and in games that support hot joining.

Beyond that, the constraint that shapes adoption is the Bevy version pairing. The plugin tracks the engine closely, so upgrading Bevy means upgrading the plugin, and the table shows that a given plugin release supports exactly one Bevy release in most cases. Projects that want to stay on an older Bevy are pinned to an older plugin, which in turn means an older feature set.

The crate is a dependency with a fair amount of surface, but the trade it offers is clear. If a project reads two keys in one system, the plugin is more than it needs. If a project wants rebinding, multiple input devices, local multiplayer, testability and network play, the alternatives are all of the work this crate has already done.

Licensing, releases and the process around the crate

The licence is dual, MIT OR Apache-2.0, with both texts in the repository. That is a permissive combination, so the crate can be used in proprietary games, and the choice of Apache-2.0 in particular brings an explicit patent grant. This describes the published terms rather than legal advice.

There are no GitHub releases attached to the repository, but the crate is published to crates.io, which is where the versions listed in the compatibility table come from, and the manifest carries the current version 0.22.0. The release process is documented in the repository rather than left implicit, with RELEASES.md holding the release notes and RELEASE-CHECKLIST.md the procedure to follow. The repository is not archived and the last push was on 2026-10-15, so the crate tracks Bevy's release cycle closely, which is also visible in the version table.

The tooling around it shows a maintained project. There is a clippy.toml at the root, a CI package in the workspace, a CONTRIBUTING.md, a markdownlint configuration, and a .github directory. The dev profile opt-level setting and the forbid(missing_docs) policy both point the same way, at a crate where the public API is expected to stay documented as it grows.

The one thing the documentation asks you to read that is easy to skip is the features section of the Cargo.toml, which the README links directly. That section is the real configuration surface of the crate, and choosing the default feature set when your game only needs keyboard and mouse means pulling in gamepad support, the gilrs backend and picking integration for nothing.

Editorial conclusion

Adopt leafwing-input-manager when a Bevy project has outgrown ad hoc key bindings, when it needs rebindable controls, local multiplayer on separate entities, or a wire format for input state, since the ActionState and ActionDiff models cover those cases directly. It is the wrong tool if you need gamepads to be detected without any setup, because assignment is manual, and it is a heavy dependency for a project that only reads a couple of keys. Before adopting, line up the crate version with your Bevy version using the compatibility table, since 0.22 of the plugin targets Bevy 0.20, and check which of the default feature flags you actually need.

Frequently asked questions

What does leafwing-input-manager do for a Bevy game?

It collects keyboard, mouse and gamepad input into an ActionState component on the player entity, using the bindings in an InputMap component to translate raw inputs into logical actions you define as an enum with the Actionlike trait.

Which version of leafwing-input-manager matches my Bevy version?

The README carries a compatibility table: Bevy 0.20 with plugin 0.22, Bevy 0.19 with 0.21, Bevy 0.18 with 0.20, Bevy 0.17 with 0.18 and 0.19, and older pairings below that. The crate in the repository is version 0.22.0 and targets Bevy 0.20.0.

Does leafwing-input-manager support multiple players on one machine?

Yes. Because the input map and the action state are components, the README describes binding keys to distinct entities for local multiplayer rather than relying on global state. Adding the plugin more than once with different action types also gives you disjoint action sets.

How are key chords and conflicting bindings handled?

Arbitrary button combinations are supported through ButtonlikeChord, and the ClashStrategy enum controls disambiguation, stopping individual buttons from triggering when a chord was intended. The clash_handling.rs example in the examples directory demonstrates the strategies.

What is the main limitation of leafwing-input-manager?

The README lists one: gamepads must be manually assigned to each input map, by reading the Gamepads resource and calling InputMap::set_gamepad. Nothing assigns a connected controller to a player entity automatically.

Official sources

  1. Issues
  2. Leafwing-Studios/leafwing-input-manager on GitHub
  3. License: Apache-2.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/leafwing-studios-leafwing-input-manager.svg)](https://hysenlabs.com/projects/leafwing-studios-leafwing-input-manager)

Was this useful?