diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index b0237464b60..bb72d5b31e0 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -285,6 +285,11 @@ class _GameWidgetState extends State> { KeyEventResult _handleKeyEvent(FocusNode focusNode, RawKeyEvent event) { final game = widget.game; + + if(!_focusNode.hasPrimaryFocus) { + return KeyEventResult.ignored; + } + if (game is KeyboardEvents) { return game.onKeyEvent(event, RawKeyboard.instance.keysPressed); } @@ -325,48 +330,51 @@ class _GameWidgetState extends State> { // We can use Directionality.maybeOf when that method lands on stable final textDir = widget.textDirection ?? TextDirection.ltr; - return Focus( - focusNode: _focusNode, - autofocus: widget.autofocus, - onKey: _handleKeyEvent, - child: MouseRegion( - cursor: currentGame.mouseCursor, - child: Directionality( - textDirection: textDir, - child: Container( - color: currentGame.backgroundColor(), - child: LayoutBuilder( - builder: (_, BoxConstraints constraints) { - return _protectedBuild(() { - final size = constraints.biggest.toVector2(); - if (size.isZero()) { - return widget.loadingBuilder?.call(context) ?? - Container(); - } - currentGame.onGameResize(size); - return FutureBuilder( - future: loaderFuture, - builder: (_, snapshot) { - if (snapshot.hasError) { - final errorBuilder = widget.errorBuilder; - if (errorBuilder == null) { - throw Error.throwWithStackTrace( - snapshot.error!, - snapshot.stackTrace!, - ); - } else { - return errorBuilder(context, snapshot.error!); - } - } - if (snapshot.connectionState == ConnectionState.done) { - return Stack(children: stackedWidgets); - } + return FocusScope( + child: Focus( + focusNode: _focusNode, + autofocus: widget.autofocus, + descendantsAreFocusable: true, + onKey: _handleKeyEvent, + child: MouseRegion( + cursor: currentGame.mouseCursor, + child: Directionality( + textDirection: textDir, + child: Container( + color: currentGame.backgroundColor(), + child: LayoutBuilder( + builder: (_, BoxConstraints constraints) { + return _protectedBuild(() { + final size = constraints.biggest.toVector2(); + if (size.isZero()) { return widget.loadingBuilder?.call(context) ?? Container(); - }, - ); - }); - }, + } + currentGame.onGameResize(size); + return FutureBuilder( + future: loaderFuture, + builder: (_, snapshot) { + if (snapshot.hasError) { + final errorBuilder = widget.errorBuilder; + if (errorBuilder == null) { + throw Error.throwWithStackTrace( + snapshot.error!, + snapshot.stackTrace!, + ); + } else { + return errorBuilder(context, snapshot.error!); + } + } + if (snapshot.connectionState == ConnectionState.done) { + return Stack(children: stackedWidgets); + } + return widget.loadingBuilder?.call(context) ?? + Container(); + }, + ); + }); + }, + ), ), ), ), diff --git a/packages/flame/test/game/game_widget/game_widget_test.dart b/packages/flame/test/game/game_widget/game_widget_test.dart index 2244feb682b..385cbbbc938 100644 --- a/packages/flame/test/game/game_widget/game_widget_test.dart +++ b/packages/flame/test/game/game_widget/game_widget_test.dart @@ -1,6 +1,8 @@ +import 'package:flame/events.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; class _Wrapper extends StatefulWidget { @@ -16,6 +18,21 @@ class _Wrapper extends StatefulWidget { State<_Wrapper> createState() => _WrapperState(); } +class _GameWithKeyboardEvents extends FlameGame with KeyboardEvents { + final List keyEvents = []; + + _GameWithKeyboardEvents(); + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + keyEvents.add(event.logicalKey); + return KeyEventResult.handled; + } +} + class _WrapperState extends State<_Wrapper> { late bool _open; @@ -221,4 +238,240 @@ void main() { expect(game2, isNull); }); }); + group('focus', () { + testWidgets('autofocus starts focused', (tester) async { + final gameFocusNode = FocusNode(); + + await tester.pumpWidget( + GameWidget( + focusNode: gameFocusNode, + game: FlameGame(), + // ignore: avoid_redundant_argument_values + autofocus: true, + ), + ); + + expect(gameFocusNode.hasFocus, isTrue); + }); + + testWidgets('autofocus false does not start focused', (tester) async { + final gameFocusNode = FocusNode(); + + await tester.pumpWidget( + GameWidget( + focusNode: gameFocusNode, + game: FlameGame(), + autofocus: false, + ), + ); + + expect(gameFocusNode.hasFocus, isFalse); + }); + + group('overlay with focus', () { + testWidgets('autofocus on overlay', (tester) async { + final gameFocusNode = FocusNode(); + final overlayFocusNode = FocusNode(); + + final game = FlameGame(); + + await tester.pumpWidget( + GameWidget( + focusNode: gameFocusNode, + game: game, + autofocus: false, + initialActiveOverlays: const ['some-overlay'], + overlayBuilderMap: { + 'some-overlay': (buildContext, game) { + return Focus( + focusNode: overlayFocusNode, + autofocus: true, + child: const SizedBox.shrink(), + ); + } + }, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + expect(gameFocusNode.hasPrimaryFocus, isFalse); + expect(gameFocusNode.hasFocus, isTrue); + expect(overlayFocusNode.hasPrimaryFocus, isTrue); + }); + + testWidgets( + 'focus goes back to game when overlays is removed', + (tester) async { + final gameFocusNode = FocusNode(); + final overlayFocusNode = FocusNode(); + + final game = FlameGame(); + + await tester.pumpWidget( + GameWidget( + focusNode: gameFocusNode, + game: game, + initialActiveOverlays: const ['some-overlay'], + overlayBuilderMap: { + 'some-overlay': (buildContext, game) { + return Focus( + focusNode: overlayFocusNode, + child: const SizedBox.shrink(), + ); + } + }, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + expect(overlayFocusNode.hasFocus, isFalse); + expect(gameFocusNode.hasPrimaryFocus, isTrue); + + overlayFocusNode.requestFocus(); + await tester.pump(); + + expect(overlayFocusNode.hasFocus, isTrue); + expect(gameFocusNode.hasPrimaryFocus, isFalse); + + game.overlays.remove('some-overlay'); + + await tester.pump(); + + expect(overlayFocusNode.hasFocus, isFalse); + expect(gameFocusNode.hasPrimaryFocus, isTrue); + }, + ); + + testWidgets('autofocus on overlay', (tester) async { + final gameFocusNode = FocusNode(); + final overlayFocusNode = FocusNode(); + + final game = FlameGame(); + + await tester.pumpWidget( + GameWidget( + focusNode: gameFocusNode, + game: game, + autofocus: false, + initialActiveOverlays: const ['some-overlay'], + overlayBuilderMap: { + 'some-overlay': (buildContext, game) { + return Focus( + focusNode: overlayFocusNode, + autofocus: true, + child: const SizedBox.shrink(), + ); + } + }, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + expect(gameFocusNode.hasPrimaryFocus, isFalse); + expect(gameFocusNode.hasFocus, isTrue); + expect(overlayFocusNode.hasPrimaryFocus, isTrue); + }); + }); + }); + + group('keyboard events', () { + testWidgets('handles keys when game is KeyboardKeys', (tester) async { + final game = _GameWithKeyboardEvents(); + + await tester.pumpWidget( + GameWidget( + game: game, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + await simulateKeyDownEvent(LogicalKeyboardKey.keyA); + await tester.pump(); + + expect(game.keyEvents, [LogicalKeyboardKey.keyA]); + }); + + testWidgets( + 'handles keys when focused regardless of being KeyboardKeys', + (tester) async { + final game = FlameGame(); + + await tester.pumpWidget( + GameWidget( + game: game, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + final handled = await simulateKeyDownEvent(LogicalKeyboardKey.keyA); + + expect(handled, isTrue); + }, + ); + + testWidgets('handles keys when KeyboardEvents', (tester) async { + final game = _GameWithKeyboardEvents(); + + await tester.pumpWidget( + GameWidget( + game: game, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + await simulateKeyDownEvent(LogicalKeyboardKey.keyA); + await tester.pump(); + + expect(game.keyEvents, [LogicalKeyboardKey.keyA]); + }); + + testWidgets('overlay handles keys', (tester) async { + final overlayKeyEvents = []; + final overlayFocusNode = FocusNode(onKey: (_, keyEvent) { + overlayKeyEvents.add(keyEvent.logicalKey); + return KeyEventResult.ignored; + }); + + final game = _GameWithKeyboardEvents(); + + await tester.pumpWidget( + GameWidget( + autofocus: false, + game: game, + initialActiveOverlays: const ['some-overlay'], + overlayBuilderMap: { + 'some-overlay': (buildContext, game) { + return Focus( + focusNode: overlayFocusNode, + autofocus: true, + child: const SizedBox.shrink(), + ); + } + }, + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + expect(overlayFocusNode.hasPrimaryFocus, isTrue); + await simulateKeyDownEvent(LogicalKeyboardKey.keyA); + await tester.pump(); + + expect(game.keyEvents, []); + expect(overlayKeyEvents, [LogicalKeyboardKey.keyA]); + }); + }); }