diff --git a/.gitignore b/.gitignore index 6176d9087..078133db5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ desktop/ build/ coverage + +.fvm diff --git a/doc/gesture-input.md b/doc/gesture-input.md index aba41bddb..bab74c853 100644 --- a/doc/gesture-input.md +++ b/doc/gesture-input.md @@ -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` diff --git a/examples/lib/stories/input/input.dart b/examples/lib/stories/input/input.dart index 260df7b9e..798cdc9d6 100644 --- a/examples/lib/stories/input/input.dart +++ b/examples/lib/stories/input/input.dart @@ -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()), diff --git a/examples/lib/stories/input/mouse_cursor.dart b/examples/lib/stories/input/mouse_cursor.dart new file mode 100644 index 000000000..b66414d07 --- /dev/null +++ b/examples/lib/stories/input/mouse_cursor.dart @@ -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; + } + } +} diff --git a/packages/flame/.min_coverage b/packages/flame/.min_coverage index e7b76e6bf..621537af0 100644 --- a/packages/flame/.min_coverage +++ b/packages/flame/.min_coverage @@ -1 +1 @@ -54.8 +57.0 diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index 566b93b1d..7876dfd03 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -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 diff --git a/packages/flame/lib/src/game/game.dart b/packages/flame/lib/src/game/game.dart index c3127ae28..45eeda897 100644 --- a/packages/flame/lib/src/game/game.dart +++ b/packages/flame/lib/src/game/game.dart @@ -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(null); } /// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance. 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 ae27b55f0..006e9388d 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -68,6 +68,10 @@ class GameWidget 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 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 extends StatefulWidget { class _GameWidgetState extends State> { Set initialActiveOverlays = {}; + MouseCursor? _mouseCursor; + Future? _gameLoaderFuture; Future get _gameLoaderFutureCache => @@ -139,8 +146,18 @@ class _GameWidgetState extends State> { // 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 extends State> { // 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 extends State> { 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 extends State> { // 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(); + }, + ); + }, + ), ), ), ), diff --git a/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart b/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart new file mode 100644 index 000000000..6ea24f167 --- /dev/null +++ b/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart @@ -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, + ); + }); + }); +}