Skip to content

Commit

Permalink
fix: focus stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
Renan Araujo committed Jun 12, 2022
1 parent 987a44f commit 87faaf6
Show file tree
Hide file tree
Showing 2 changed files with 302 additions and 40 deletions.
89 changes: 49 additions & 40 deletions packages/flame/lib/src/game/game_widget/game_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {

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);
}
Expand Down Expand Up @@ -325,48 +330,52 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
// 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();
},
);
});
},
),
),
),
),
Expand Down
253 changes: 253 additions & 0 deletions packages/flame/test/game/game_widget/game_widget_test.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,6 +18,21 @@ class _Wrapper extends StatefulWidget {
State<_Wrapper> createState() => _WrapperState();
}

class _GameWithKeyboardEvents extends FlameGame with KeyboardEvents {
final List<LogicalKeyboardKey> keyEvents = [];

_GameWithKeyboardEvents();

@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
keyEvents.add(event.logicalKey);
return KeyEventResult.handled;
}
}

class _WrapperState extends State<_Wrapper> {
late bool _open;

Expand Down Expand Up @@ -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 = <LogicalKeyboardKey>[];
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, <RawKeyEvent>[]);
expect(overlayKeyEvents, [LogicalKeyboardKey.keyA]);
});
});
}

0 comments on commit 87faaf6

Please sign in to comment.