Adding custom mouse cursors for flame (#935)

* Adding custom mouse cursors for flame

* linting and adding fvm to gitignore

* PR suggestions

* Apply suggestions from code review

Co-authored-by: Luan Nico <luanpotter27@gmail.com>

Co-authored-by: Luan Nico <luanpotter27@gmail.com>
This commit is contained in:
Erick
2021-09-05 20:55:26 -03:00
committed by GitHub
parent c015af8bee
commit 5529869a76
9 changed files with 221 additions and 32 deletions

2
.gitignore vendored
View File

@ -18,3 +18,5 @@ desktop/
build/
coverage
.fvm

View File

@ -102,6 +102,22 @@ and [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-cla
also read more about Flutter's gestures
[here](https://api.flutter.dev/flutter/gestures/gestures-library.html).
It is also possible to change the current mouse cursor displayed on the `GameWidget` region. To do
so the following code can be used inside the `Game` class
```dart
mouseCursor.value = SystemMouseCursors.move;
```
To already initialize the `GameWidget` with a custom cursor, the `mouseCursor` property can be used
```dart
GameWidget(
game: MouseCursorGame(),
mouseCursor: SystemMouseCursors.move,
);
```
## Event coordinate system
On events that have positions, like for example `Tap*` or `Drag`, you will notice that the `eventPosition`

View File

@ -1,5 +1,6 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import '../../commons/commons.dart';
import 'draggables.dart';
@ -7,6 +8,7 @@ import 'hoverables.dart';
import 'joystick.dart';
import 'joystick_advanced.dart';
import 'keyboard.dart';
import 'mouse_cursor.dart';
import 'mouse_movement.dart';
import 'multitap.dart';
import 'multitap_advanced.dart';
@ -26,6 +28,18 @@ void addInputStories(Dashbook dashbook) {
(_) => GameWidget(game: MouseMovementGame()),
codeLink: baseLink('input/mouse_movement.dart'),
)
..add(
'Mouse Cursor',
(_) => GameWidget(
game: MouseCursorGame(),
mouseCursor: SystemMouseCursors.move,
),
codeLink: baseLink('input/mouse_cursor.dart'),
info: '''
Example showcasing the ability to change the game cursor in runtime
hover the little square to see the cursor changing
''',
)
..add(
'Scroll',
(_) => GameWidget(game: ScrollGame()),

View File

@ -0,0 +1,53 @@
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class MouseCursorGame extends Game with MouseMovementDetector {
static const speed = 200;
static final Paint _blue = BasicPalette.blue.paint();
static final Paint _white = BasicPalette.white.paint();
static final Vector2 objSize = Vector2.all(150);
Vector2 position = Vector2(100, 100);
Vector2? target;
bool onTarget = false;
@override
void onMouseMove(PointerHoverInfo info) {
target = info.eventPosition.game;
}
Rect _toRect() => position.toPositionedRect(objSize);
@override
void render(Canvas canvas) {
canvas.drawRect(
_toRect(),
onTarget ? _blue : _white,
);
}
@override
void update(double dt) {
final target = this.target;
if (target != null) {
final hovering = _toRect().contains(target.toOffset());
if (hovering) {
if (!onTarget) {
//Entered
mouseCursor.value = SystemMouseCursors.grab;
}
} else {
if (onTarget) {
// Exited
mouseCursor.value = SystemMouseCursors.move;
}
}
onTarget = hovering;
}
}
}

View File

@ -1 +1 @@
54.8
57.0

View File

@ -44,6 +44,7 @@
- Add `loadAllImages` to `Images`, which loads all images from the prefixed path
- Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`)
- Added `FocusNode` on the game widget and improved keyboard handling in the game.
- Added ability to have custom mouse cursor on the `GameWidget` region
## [1.0.0-releasecandidate.13]
- Fix camera not ending up in the correct position on long jumps

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
@ -209,6 +210,11 @@ abstract class Game extends Projector {
/// - GameWidget
/// - [Game.overlays]
final overlays = ActiveOverlaysNotifier();
/// Used to change the mouse cursor of the GameWidget running this game.
/// Setting the value to null will make the GameWidget defer the choice
/// of the cursor to the closest region available on the tree.
final mouseCursor = ValueNotifier<MouseCursor?>(null);
}
/// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance.

View File

@ -68,6 +68,10 @@ class GameWidget<T extends Game> extends StatefulWidget {
/// Defaults to true.
final bool autofocus;
/// Initial mouse cursor for this [GameWidget]
/// mouse cursor can be changed in runtime using [Game.mouseCursor]
final MouseCursor? mouseCursor;
/// Renders a [game] in a flutter widget tree.
///
/// Ex:
@ -116,6 +120,7 @@ class GameWidget<T extends Game> extends StatefulWidget {
this.initialActiveOverlays,
this.focusNode,
this.autofocus = true,
this.mouseCursor,
}) : super(key: key);
/// Renders a [game] in a flutter widget tree alongside widgets overlays.
@ -128,6 +133,8 @@ class GameWidget<T extends Game> extends StatefulWidget {
class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
Set<String> initialActiveOverlays = {};
MouseCursor? _mouseCursor;
Future<void>? _gameLoaderFuture;
Future<void> get _gameLoaderFutureCache =>
@ -139,8 +146,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
// Add the initial overlays
_initActiveOverlays();
addOverlaysListener();
addOverlaysListener(widget.game);
// Add the initial mouse cursor
_initMouseCursor();
addMouseCursorListener();
}
void _initMouseCursor() {
if (widget.mouseCursor != null) {
widget.game.mouseCursor.value = widget.mouseCursor;
_mouseCursor = widget.game.mouseCursor.value;
}
}
void _initActiveOverlays() {
@ -161,7 +178,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
// Reset the overlays
_initActiveOverlays();
addOverlaysListener(widget.game);
addOverlaysListener();
// Reset mouse cursor
_initMouseCursor();
addMouseCursorListener();
// Reset the loader future
_gameLoaderFuture = null;
@ -174,8 +195,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
removeOverlaysListener(widget.game);
}
void addMouseCursorListener() {
widget.game.mouseCursor.addListener(onChangeMouseCursor);
}
void onChangeMouseCursor() {
setState(() {
_mouseCursor = widget.game.mouseCursor.value;
});
}
// widget overlay stuff
void addOverlaysListener(T game) {
void addOverlaysListener() {
widget.game.overlays.addListener(onChangeActiveOverlays);
initialActiveOverlays = widget.game.overlays.value;
}
@ -249,35 +280,38 @@ 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: widget.focusNode,
autofocus: widget.autofocus,
onKey: _handleKeyEvent,
child: Directionality(
textDirection: textDir,
child: Container(
color: widget.game.backgroundColor(),
child: LayoutBuilder(
builder: (_, BoxConstraints constraints) {
widget.game.onResize(constraints.biggest.toVector2());
return FutureBuilder(
future: _gameLoaderFutureCache,
builder: (_, snapshot) {
if (snapshot.hasError) {
final errorBuilder = widget.errorBuilder;
if (errorBuilder == null) {
throw snapshot.error!;
} else {
return errorBuilder(context, snapshot.error!);
return MouseRegion(
cursor: _mouseCursor ?? MouseCursor.defer,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
onKey: _handleKeyEvent,
child: Directionality(
textDirection: textDir,
child: Container(
color: widget.game.backgroundColor(),
child: LayoutBuilder(
builder: (_, BoxConstraints constraints) {
widget.game.onResize(constraints.biggest.toVector2());
return FutureBuilder(
future: _gameLoaderFutureCache,
builder: (_, snapshot) {
if (snapshot.hasError) {
final errorBuilder = widget.errorBuilder;
if (errorBuilder == null) {
throw snapshot.error!;
} else {
return errorBuilder(context, snapshot.error!);
}
}
}
if (snapshot.connectionState == ConnectionState.done) {
return Stack(children: stackedWidgets);
}
return widget.loadingBuilder?.call(context) ?? Container();
},
);
},
if (snapshot.connectionState == ConnectionState.done) {
return Stack(children: stackedWidgets);
}
return widget.loadingBuilder?.call(context) ?? Container();
},
);
},
),
),
),
),

View File

@ -0,0 +1,63 @@
import 'dart:ui';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class TestGame extends Game {
@override
void render(Canvas canvas) {}
@override
void update(double dt) {}
}
Finder byMouseCursor(MouseCursor cursor) {
return find.byWidgetPredicate(
(widget) => widget is MouseRegion && widget.cursor == cursor,
);
}
void main() {
group('GameWidget - MouseCursor', () {
testWidgets('renders with the initial cursor', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: GameWidget(
game: TestGame(),
mouseCursor: SystemMouseCursors.grab,
),
),
);
expect(
byMouseCursor(SystemMouseCursors.grab),
findsOneWidget,
);
});
testWidgets('can change the cursor', (tester) async {
final game = TestGame();
await tester.pumpWidget(
MaterialApp(
home: GameWidget(
game: game,
mouseCursor: SystemMouseCursors.grab,
),
),
);
// Making sure this cursor isn't showing yet
expect(byMouseCursor(SystemMouseCursors.copy), findsNothing);
game.mouseCursor.value = SystemMouseCursors.copy;
await tester.pump();
expect(
byMouseCursor(SystemMouseCursors.copy),
findsOneWidget,
);
});
});
}