mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 00:48:47 +08:00 
			
		
		
		
	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:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -18,3 +18,5 @@ desktop/ | |||||||
| build/ | build/ | ||||||
|  |  | ||||||
| coverage | coverage | ||||||
|  |  | ||||||
|  | .fvm | ||||||
|  | |||||||
| @ -102,6 +102,22 @@ and [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-cla | |||||||
| also read more about Flutter's gestures | also read more about Flutter's gestures | ||||||
| [here](https://api.flutter.dev/flutter/gestures/gestures-library.html). | [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 | ## Event coordinate system | ||||||
|  |  | ||||||
| On events that have positions, like for example `Tap*` or `Drag`, you will notice that the `eventPosition` | On events that have positions, like for example `Tap*` or `Drag`, you will notice that the `eventPosition` | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import 'package:dashbook/dashbook.dart'; | import 'package:dashbook/dashbook.dart'; | ||||||
| import 'package:flame/game.dart'; | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
| import '../../commons/commons.dart'; | import '../../commons/commons.dart'; | ||||||
| import 'draggables.dart'; | import 'draggables.dart'; | ||||||
| @ -7,6 +8,7 @@ import 'hoverables.dart'; | |||||||
| import 'joystick.dart'; | import 'joystick.dart'; | ||||||
| import 'joystick_advanced.dart'; | import 'joystick_advanced.dart'; | ||||||
| import 'keyboard.dart'; | import 'keyboard.dart'; | ||||||
|  | import 'mouse_cursor.dart'; | ||||||
| import 'mouse_movement.dart'; | import 'mouse_movement.dart'; | ||||||
| import 'multitap.dart'; | import 'multitap.dart'; | ||||||
| import 'multitap_advanced.dart'; | import 'multitap_advanced.dart'; | ||||||
| @ -26,6 +28,18 @@ void addInputStories(Dashbook dashbook) { | |||||||
|       (_) => GameWidget(game: MouseMovementGame()), |       (_) => GameWidget(game: MouseMovementGame()), | ||||||
|       codeLink: baseLink('input/mouse_movement.dart'), |       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( |     ..add( | ||||||
|       'Scroll', |       'Scroll', | ||||||
|       (_) => GameWidget(game: ScrollGame()), |       (_) => GameWidget(game: ScrollGame()), | ||||||
|  | |||||||
							
								
								
									
										53
									
								
								examples/lib/stories/input/mouse_cursor.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								examples/lib/stories/input/mouse_cursor.dart
									
									
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1 +1 @@ | |||||||
| 54.8 | 57.0 | ||||||
|  | |||||||
| @ -44,6 +44,7 @@ | |||||||
|  - Add `loadAllImages` to `Images`, which loads all images from the prefixed path |  - Add `loadAllImages` to `Images`, which loads all images from the prefixed path | ||||||
|  - Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`) |  - Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`) | ||||||
|  - Added `FocusNode` on the game widget and improved keyboard handling in the game. |  - 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] | ## [1.0.0-releasecandidate.13] | ||||||
|  - Fix camera not ending up in the correct position on long jumps |  - Fix camera not ending up in the correct position on long jumps | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/rendering.dart'; | import 'package:flutter/rendering.dart'; | ||||||
| import 'package:flutter/scheduler.dart'; | import 'package:flutter/scheduler.dart'; | ||||||
| import 'package:flutter/widgets.dart'; | import 'package:flutter/widgets.dart'; | ||||||
| @ -209,6 +210,11 @@ abstract class Game extends Projector { | |||||||
|   /// - GameWidget |   /// - GameWidget | ||||||
|   /// - [Game.overlays] |   /// - [Game.overlays] | ||||||
|   final overlays = ActiveOverlaysNotifier(); |   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. | /// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance. | ||||||
|  | |||||||
| @ -68,6 +68,10 @@ class GameWidget<T extends Game> extends StatefulWidget { | |||||||
|   /// Defaults to true. |   /// Defaults to true. | ||||||
|   final bool autofocus; |   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. |   /// Renders a [game] in a flutter widget tree. | ||||||
|   /// |   /// | ||||||
|   /// Ex: |   /// Ex: | ||||||
| @ -116,6 +120,7 @@ class GameWidget<T extends Game> extends StatefulWidget { | |||||||
|     this.initialActiveOverlays, |     this.initialActiveOverlays, | ||||||
|     this.focusNode, |     this.focusNode, | ||||||
|     this.autofocus = true, |     this.autofocus = true, | ||||||
|  |     this.mouseCursor, | ||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   /// Renders a [game] in a flutter widget tree alongside widgets overlays. |   /// 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>> { | class _GameWidgetState<T extends Game> extends State<GameWidget<T>> { | ||||||
|   Set<String> initialActiveOverlays = {}; |   Set<String> initialActiveOverlays = {}; | ||||||
|  |  | ||||||
|  |   MouseCursor? _mouseCursor; | ||||||
|  |  | ||||||
|   Future<void>? _gameLoaderFuture; |   Future<void>? _gameLoaderFuture; | ||||||
|  |  | ||||||
|   Future<void> get _gameLoaderFutureCache => |   Future<void> get _gameLoaderFutureCache => | ||||||
| @ -139,8 +146,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> { | |||||||
|  |  | ||||||
|     // Add the initial overlays |     // Add the initial overlays | ||||||
|     _initActiveOverlays(); |     _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() { |   void _initActiveOverlays() { | ||||||
| @ -161,7 +178,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> { | |||||||
|  |  | ||||||
|       // Reset the overlays |       // Reset the overlays | ||||||
|       _initActiveOverlays(); |       _initActiveOverlays(); | ||||||
|       addOverlaysListener(widget.game); |       addOverlaysListener(); | ||||||
|  |  | ||||||
|  |       // Reset mouse cursor | ||||||
|  |       _initMouseCursor(); | ||||||
|  |       addMouseCursorListener(); | ||||||
|  |  | ||||||
|       // Reset the loader future |       // Reset the loader future | ||||||
|       _gameLoaderFuture = null; |       _gameLoaderFuture = null; | ||||||
| @ -174,8 +195,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> { | |||||||
|     removeOverlaysListener(widget.game); |     removeOverlaysListener(widget.game); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void addMouseCursorListener() { | ||||||
|  |     widget.game.mouseCursor.addListener(onChangeMouseCursor); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void onChangeMouseCursor() { | ||||||
|  |     setState(() { | ||||||
|  |       _mouseCursor = widget.game.mouseCursor.value; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // widget overlay stuff |   // widget overlay stuff | ||||||
|   void addOverlaysListener(T game) { |   void addOverlaysListener() { | ||||||
|     widget.game.overlays.addListener(onChangeActiveOverlays); |     widget.game.overlays.addListener(onChangeActiveOverlays); | ||||||
|     initialActiveOverlays = widget.game.overlays.value; |     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 |     // We can use Directionality.maybeOf when that method lands on stable | ||||||
|     final textDir = widget.textDirection ?? TextDirection.ltr; |     final textDir = widget.textDirection ?? TextDirection.ltr; | ||||||
|  |  | ||||||
|     return Focus( |     return MouseRegion( | ||||||
|       focusNode: widget.focusNode, |       cursor: _mouseCursor ?? MouseCursor.defer, | ||||||
|       autofocus: widget.autofocus, |       child: Focus( | ||||||
|       onKey: _handleKeyEvent, |         focusNode: widget.focusNode, | ||||||
|       child: Directionality( |         autofocus: widget.autofocus, | ||||||
|         textDirection: textDir, |         onKey: _handleKeyEvent, | ||||||
|         child: Container( |         child: Directionality( | ||||||
|           color: widget.game.backgroundColor(), |           textDirection: textDir, | ||||||
|           child: LayoutBuilder( |           child: Container( | ||||||
|             builder: (_, BoxConstraints constraints) { |             color: widget.game.backgroundColor(), | ||||||
|               widget.game.onResize(constraints.biggest.toVector2()); |             child: LayoutBuilder( | ||||||
|               return FutureBuilder( |               builder: (_, BoxConstraints constraints) { | ||||||
|                 future: _gameLoaderFutureCache, |                 widget.game.onResize(constraints.biggest.toVector2()); | ||||||
|                 builder: (_, snapshot) { |                 return FutureBuilder( | ||||||
|                   if (snapshot.hasError) { |                   future: _gameLoaderFutureCache, | ||||||
|                     final errorBuilder = widget.errorBuilder; |                   builder: (_, snapshot) { | ||||||
|                     if (errorBuilder == null) { |                     if (snapshot.hasError) { | ||||||
|                       throw snapshot.error!; |                       final errorBuilder = widget.errorBuilder; | ||||||
|                     } else { |                       if (errorBuilder == null) { | ||||||
|                       return errorBuilder(context, snapshot.error!); |                         throw snapshot.error!; | ||||||
|  |                       } else { | ||||||
|  |                         return errorBuilder(context, snapshot.error!); | ||||||
|  |                       } | ||||||
|                     } |                     } | ||||||
|                   } |                     if (snapshot.connectionState == ConnectionState.done) { | ||||||
|                   if (snapshot.connectionState == ConnectionState.done) { |                       return Stack(children: stackedWidgets); | ||||||
|                     return Stack(children: stackedWidgets); |                     } | ||||||
|                   } |                     return widget.loadingBuilder?.call(context) ?? Container(); | ||||||
|                   return widget.loadingBuilder?.call(context) ?? Container(); |                   }, | ||||||
|                 }, |                 ); | ||||||
|               ); |               }, | ||||||
|             }, |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  | |||||||
| @ -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, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Erick
					Erick