Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
d0f72e3
feat: add package flutter_gamepads with a widget that emits intents b…
lea108 Mar 28, 2026
2daf11a
remove: some old wip code not intented to be included
lea108 Mar 28, 2026
92c0349
docs: Basic usage info in README.md
lea108 Mar 28, 2026
ea9fcf7
feat: GamepadInterceptor and updated example
lea108 Mar 29, 2026
72f89b5
docs: Update REAdME.md
lea108 Mar 29, 2026
d20a718
test: Add test for GamepadInterceptor
lea108 Mar 29, 2026
c8902c7
feat: Input repeat
lea108 Mar 29, 2026
d4de40c
refactor: place gamepad_activator in api folder
lea108 Mar 29, 2026
11ebc07
fix: Forgot change
lea108 Mar 29, 2026
b9187b7
fix: analyze/format
lea108 Mar 29, 2026
87c3fc3
remove: unused code
lea108 Mar 29, 2026
b7a0951
chore: format code
lea108 Mar 29, 2026
c2b49bc
chore: format README.md
lea108 Mar 29, 2026
cb477fc
feat: a pure flutter game in the example of flutter_gamepads
lea108 Apr 1, 2026
8d51ba5
feat: send GamepadActivator to onBeforeIntent()
lea108 Apr 1, 2026
d399955
add: A Flame game example
lea108 Apr 1, 2026
a6a8bcb
refactor: Merge the two example folders and add a Example chooser men…
lea108 Apr 1, 2026
9f54e83
fix: Cancel timers in GamepadControl in dispose()
lea108 Apr 1, 2026
975927e
fix: Cancel timers also on ignoreEvents=true
lea108 Apr 1, 2026
4754863
fix: Trap focus in dialogs without focusing a button
lea108 Apr 1, 2026
5daa77e
docs: Add Flame specific guidance to README
lea108 Apr 1, 2026
480c19a
docs: Add note to note have multiple active GamepadControl in tree at…
lea108 Apr 1, 2026
fd0e2ec
refactor: Extract SliderWithGamepadSupport widget
lea108 Apr 1, 2026
a68e2c6
docs: provide more in-app guidance in Flutter example
lea108 Apr 1, 2026
2e2c5c9
docs: Refer to SliderWithGamepadSupport from README
lea108 Apr 1, 2026
740fafe
feat: list of repeatable intents
lea108 Apr 1, 2026
02c9b68
docs: update README.md
lea108 Apr 1, 2026
3eb40ca
docs: update links
lea108 Apr 1, 2026
0586411
chore: dart analyze/fix/format
lea108 Apr 1, 2026
f615e28
fix: downgrade Flame for melos package constraints
lea108 Apr 1, 2026
71468a7
fix: make power up in example game look more like a lightning and les…
lea108 Apr 1, 2026
e421330
add: show icons in example selector
lea108 Apr 1, 2026
b35fc46
chore: Fix dart anazyle
lea108 Apr 1, 2026
f08ac2c
chore: Add emty line at end of README.md
lea108 Apr 2, 2026
73cf9d2
fix: left vs right in example
lea108 Apr 2, 2026
52b84a0
refactor: tidy up the example switcher and document key parts of exam…
lea108 Apr 2, 2026
fec1925
change: remove autofocus from flutter example to make it look and fee…
lea108 Apr 2, 2026
f3150d9
docs: Update the intro of README
lea108 Apr 2, 2026
c1c2183
chore: change ul style to dash in README
lea108 Apr 2, 2026
da626cf
chore: Fix line length in README.md
lea108 Apr 2, 2026
03dab9c
docs: update descirption in main README.md
lea108 Apr 2, 2026
5405946
refactor: extract component files in flame example and improve docume…
lea108 Apr 2, 2026
43cbaac
chore: format code
lea108 Apr 2, 2026
3e515e0
docs: Update flutter_gamepads README (major update)
lea108 Apr 2, 2026
2bf905b
chore: markdown format
lea108 Apr 3, 2026
add5783
chore: markdown format
lea108 Apr 3, 2026
d76a27d
chore: markdown format
lea108 Apr 3, 2026
98d93f4
chore: line length
lea108 Apr 3, 2026
8873ff6
docs: slim features section
lea108 Apr 3, 2026
de18cdd
docs: slim 'Not included'
lea108 Apr 3, 2026
87bc233
docs: tweak intro + slim features
lea108 Apr 3, 2026
00e37c7
docs: tweak
lea108 Apr 3, 2026
ff4c1f2
docs: tweak
lea108 Apr 3, 2026
2855752
docs: move diagram down
lea108 Apr 3, 2026
937e3da
docs: add back GamepadControl code snippet for visual balance
lea108 Apr 3, 2026
9832e3a
docs: focus more on usage and less on how it works in README.md
lea108 Apr 3, 2026
c4c7ba4
docs: tweak
lea108 Apr 3, 2026
b09f376
docs: tweak
lea108 Apr 3, 2026
1bd4928
docs: Fix italics
lea108 Apr 3, 2026
cb7e80b
docs: provide more visual anchor in Examples heading
lea108 Apr 3, 2026
e8f0031
docs: Update package description in pubspec.yaml
lea108 Apr 3, 2026
a5a403f
docs: Fix package LISENCE file
lea108 Apr 3, 2026
4e0bae8
docs: Add H1 in CHANGELOG.md
lea108 Apr 3, 2026
c6cb2ac
chore: markdown format
lea108 Apr 3, 2026
a3486c0
Merge branch 'main' into feat/flutter-gamepads-package
lea108 Apr 3, 2026
e2a836a
docs: Fix links in README.md
lea108 Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ redistributable. This is because gamepads uses GameInput API v0
which is statically linked.


## Bridge packages

- [flame_gamepads](https://github.com/flame-engine/flame/tree/main/packages/flame_gamepads) -
Provides a GamepadCallbacks component mixin for your Flame games
- [flutter_gamepads](./packages/flutter_gamepads/) - Provides a widget that maps gamepad input
to UI interaction with your Flutter widgets. In regular Flutter apps as well as for Flame
overlays.


## Support

The simplest way to show us your support is by giving the project a star! :star:
Expand Down
31 changes: 31 additions & 0 deletions packages/flutter_gamepads/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/
10 changes: 10 additions & 0 deletions packages/flutter_gamepads/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa"
channel: "[user-branch]"

project_type: package
5 changes: 5 additions & 0 deletions packages/flutter_gamepads/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Change Log

## 0.1.0

- Initial release
1 change: 1 addition & 0 deletions packages/flutter_gamepads/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../LICENSE
255 changes: 255 additions & 0 deletions packages/flutter_gamepads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# flutter_gamepads

A Flutter package that maps gamepad input to UI interaction.

This package is built on Flutter’s focus and intent system which powers keyboard navigation in
Flutter. This means that for a large part, the same effort you spend on supporting keyboard and
screen reader users also benefit gamepad users and vice versa.

The philosophy is that you just add Gamepad support to your app to extend its multi-modality
of user input.


## Features

- Move focus
- Activate focused button
- Dismiss
- Scroll
- (actually anything that you can do with Intents in Flutter, plus more with callbacks)
- Gamepad buttons and axes can be used as input
- Input repetition (on long press/activation)
- Uses [*gamepads*](https://pub.dev/packages/gamepads) as the underlying Gamepad platforms
support library
- A GamepadControl widget to wrap your app which in some cases is all you need.
- Callbacks that allow intercepting an Intent before it actually is emitted.
- Extensive example project showing how the package can be used in pure Flutter apps
as well as for Flame game overlays.
- Can be used in both pure Flutter apps and in Flame games for overlays and menus. For Flame
usage, see the Flame-specific guidance later in the README.


### Limitations

This package does not magically "just work" in all cases. Your app has to work reasonably well
with the Flutter focus system and there can be some widgets that need some extra work to get
working.

Text input is currently not supported via Gamepad input.


## Quick-start


### Preparation

Use only TAB key and Space/Enter to navigate your app. If this works, it will likely work well
with gamepad input.

If there are widgets you cannot reach, fix that first. If there are widgets you cannot interact
with while they are focused, that can be handled specifically for gamepads.

If you cannot clearly see which widget has focus, update the theme of your app and make for
example the border of focused widgets stand out in a different color.


### Gamepad support

1. Start by wrapping your MaterialApp with `GamepadControl`, which in some cases is all you need.
2. Test your app
3. If you find out that some widgets can't be controlled with a gamepad, add an `onBeforeIntent`
callback to `GamepadControl` or wrap each widget with a `GamepadInterceptor`. See below for
detailed explanation of callbacks.


## Usage

`GamepadControl` is the main widget of this package. You usually have exactly one of this widget
that wraps your MaterialApp or similar. This widget will listen on *gamepads* input stream
and emit Flutter intents on the primary focused context based on user input.


### Callbacks

You can provide a `onBeforeIntent` method to `GamepadControl` to intercept just before an intent
would be invoked on the primary focus of your Flutter app.

If you return false from it, the intent is blocked from being emitted.

```dart
bool onBeforeIntent(GamepadActivator activator, Intent intent) {

}
```

However with local states this becomes impractical and *flutter_gamepads* provides an
other widget `GamepadInterceptor` that you wrap a subtree of widgets. It's only purpose
is to provide an onBeforeIntent callback that is locally scoped.

```dart
GamepadInterceptor(
onBeforeIntent: (activator, intent) {

},
child: YourWidget(),
)
```


#### onBeforeIntent examples

An example of how to build a gamepad-extended widget can be found in
[SliderWithGamepadExport](https://github.com/flame-engine/gamepads/tree/main/packages/flutter_gamepads/example/lib/flutter_example/pages/slider_with_gamepad_support.dart).

Another example, using the activator to support 4-way directional D-pad
within a Tick-tac-toe game is in [TicTacToe widget](https://github.com/flame-engine/gamepads/tree/main/packages/flutter_gamepads/example/lib/flutter_example/pages/game_page.dart).


### Blocking Gamepad input

There are four ways to block gamepad input from invoking intents:

1. Omitting the `GamepadControl` widget from your widget tree
- Fully unregisters `gamepad` event handles, axis activation memory, repeat timers etc.
2. `GamepadControl.ignoreEvents == true`
- Early check on each *gamepads* event, axis activation memory is reset and repeat timers are
reset.
3. `GamepadInterceptor.onBeforeIntent() => false`
- Blocks each intent before it is passed on to GamepadControl.onBeforeIntent()
4. `GamepadControl.onBeforeIntent() => false`
- Blocks each intent before it is passed on to Flutter.

Method 1 and 2 are good for when you fully want to block gamepad control of Flutter UI.

Method 3 or 4 is good if you want to block specific intents.


### Multiple GamepadControl widgets

Note that if you have multiple `GamepadControl` widgets concurrently in your widget tree, they
will all emit intents on the `primaryFocus` focus node. Except if you set `ignoreEvents` or
use `onBeforeIntent` to block intents from all but one of the `GamepadControl` widgets.

The `GamepadControl` widget does not check if primaryFocus is a descendant of itself.


### How it works

`GamepadControl` listens on `NormalizedGamepadEvent` from *gamepads* package and maps those
to a `GamepadActivator` and its related `Intent`.

Input repetition is conceptually started on activation of a GamepadActivator and stopped
once the activator has been canceled (eg. button up or axis below minimum threshold).

`GamepadControl` will lookup the closest ancestor `GamepadInterceptor` from `primaryFocus`
and call its `onBeforeIntent` first (if there is one), and then proceed to `onBeforeIntent` on
`GamepadControl`. Calling is lazy so if the local `onBeforeIntent` returns fall, the one on
`GamepadControl` is not called.

If no onBeforeIntent has rejected, the Intent will be invoked on the primary focus context.

[Diagram of the callbacks and intent emit chain](https://github.com/flame-engine/gamepads/tree/main/packages/flutter_gamepads/example/lib/flutter_example/docs/input_diagram.svg)


### Defaults

By default `GamepadControl` comes with these bindings:

- D-pad up: Previous focus
- D-pad left: Previous focus
- D-pad right: Next focus
- D-pad down: Next focus
- A: Activate
- B: Dismiss
- Right stick up: Scroll up
- Right stick left: Scroll left
- Right stick right: Scroll right
- Right stick down: Scroll down

Except for Activate and Dismiss, all intents have input repeat enabled by default.

The bindings can customized via the `shortcuts` parameter. It is not limited to the intents
above. Any class that inherits from the `Intent` base class can be used as the emitted intent
for a gamepad activator (button or axis).


### Flame specific guidance

`flutter_gamepads` can be helpful in scenarios when you have overlays in your
Flame game that you want users to be able to navigate with their gamepad.

1. Wrap your `GameWidget` with a `GamepadControl` widget
2. For overlays that represent a modal dialog, you will need to trap the focus
in the dialog. See
[OverlayDialogBackdrop](https://github.com/flame-engine/gamepads/tree/main/packages/flutter_gamepads/example/lib/flame_example/overlays/overlay_dialog_backdrop.dart)
in Flame example app for how you can do that. In that example the dialog itself
will receive the focus so that when a mouse user opens the dialog, it won't show
a focus indicator on a button in the dialog.
3. To close overlay dialogs on DismissIntent, you will need to catch it with
`onBeforeIntent` and close the overlay. In
[Flame Example](https://github.com/flame-engine/gamepads/tree/main/packages/flutter_gamepads/example/lib/flame_example/main.dart)
this is done generically at the root, but could instead wrap each dialog in a
`GamepadInterceptor` to do it locally if you need to guard closing the dialog
by some condition.
4. If you need to disable `GamepadControl` while in-game you can do so by setting
`ignoreEvents = true` on it.


## Code example

In the example folder there is both a full Flutter app and a full Flame game example showing
how the package can be used in those two scenarios.

Here follows a brief code example:

```dart
GamepadControl(
child: MaterialApp(
home: Scaffold(
body: Column(
children: [
SwitchListTile(
title: const Text('Works with gamepads'),
value: switchValue,
onChanged: (value) => setState(() => switchValue = value),
),
ElevatedButton(onPressed: () {}, Text('Can be clicked with gamepad')),
// This can be focused, but gamepad users cannot change the value
// The solution is given below.
Slider(
value: sliderValue,
label: 'Does not work with gamepads',
onChange: (value) => setState(() => sliderValue = value),
),
// This slider can be operated with Gamepad due to the
// compatibility layer provided via GamepadInterceptor.
GamepadInterceptor(
onBeforeIntent: (activator, intent) {
if (intent is ScrollIntent) {
if (intent.direction == AxisDirection.right) {
setState(() _value = min(1.0, _value + 0.1));
} else if (intent.direction ==
AxisDirection.left) {
setState(() _value = max(0.0, _value - 0.1));
}
// Block actual emit of ScrollIntent
return false;
}
// Allow other intents such as focus change to occur
return true;
},
child: Slider(
value: _value,
label: 'Works with gamepads',
max: 1.0,
// This setState never occur by Gamepad input, but is
// good to allow keyboard/mouse input as well.
onChange: (value) => setState(() => _value = value),
),
),
],
),
),
),
)
```
1 change: 1 addition & 0 deletions packages/flutter_gamepads/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:flame_lint/analysis_options.yaml
29 changes: 29 additions & 0 deletions packages/flutter_gamepads/docs/input_diagram.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
flowchart TD
input["Normalized Gamepad Input (gamepads package)"]
--> ignoreCheck{"ignoreEvents"}

ignoreCheck -->|true| stopIgnore[STOP]
ignoreCheck -->|false| findActivator[Find matching GamepadActivator]

findActivator --> keyEventType{"Activator event"}

keyEventType -->|Activated| startRepeat[Start repeat timer for intent]
keyEventType -->|Canceled| stopRepeat[Stop repeat timer of intent]
stopRepeat --> stopAfterStopRepeat[STOP]
keyEventType -->|Neither| stopInvalid[STOP]

timerEvent[Repeat timer event]
startRepeat --> findInterceptor
timerEvent --> findInterceptor

findInterceptor[Find nearest GamepadInterceptor ancestor from primaryFocus]

findInterceptor --> interceptorDecision{"GamepadInterceptor<br/>.onBeforeIntent()?"}

interceptorDecision -->|Return false| stopInterceptor[STOP]
interceptorDecision -->|Return true| controlDecision{"GamepadControl<br/>.onBeforeIntent()?"}

controlDecision -->|Return false| stopControl[STOP]
controlDecision -->|Return true| emitIntent[Emit Intent to primaryFocus]

emitIntent --> actionsSystem[Flutter Actions system handles it]
1 change: 1 addition & 0 deletions packages/flutter_gamepads/docs/input_diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading