mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 08:56:01 +08:00 
			
		
		
		
	feat: Add HoverCallbacks (#2706)
This creates HoverCallbacks (and PointerMoveCallbacks) to replicate the Hoverables behaviour in the new camera and event system.
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/.cspell/gamedev_dictionary.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/.cspell/gamedev_dictionary.txt
									
									
									
									
										vendored
									
									
								
							| @ -156,3 +156,4 @@ viewports | |||||||
| vsync | vsync | ||||||
| widget's | widget's | ||||||
| unawaited | unawaited | ||||||
|  | proxied | ||||||
| @ -18,6 +18,7 @@ import 'package:doc_flame_examples/move_to_effect.dart'; | |||||||
| import 'package:doc_flame_examples/opacity_by_effect.dart'; | import 'package:doc_flame_examples/opacity_by_effect.dart'; | ||||||
| import 'package:doc_flame_examples/opacity_effect_with_target.dart'; | import 'package:doc_flame_examples/opacity_effect_with_target.dart'; | ||||||
| import 'package:doc_flame_examples/opacity_to_effect.dart'; | import 'package:doc_flame_examples/opacity_to_effect.dart'; | ||||||
|  | import 'package:doc_flame_examples/pointer_events.dart'; | ||||||
| import 'package:doc_flame_examples/ray_cast.dart'; | import 'package:doc_flame_examples/ray_cast.dart'; | ||||||
| import 'package:doc_flame_examples/ray_trace.dart'; | import 'package:doc_flame_examples/ray_trace.dart'; | ||||||
| import 'package:doc_flame_examples/remove_effect.dart'; | import 'package:doc_flame_examples/remove_effect.dart'; | ||||||
| @ -60,6 +61,7 @@ void main() { | |||||||
|     'opacity_by_effect': OpacityByEffectGame.new, |     'opacity_by_effect': OpacityByEffectGame.new, | ||||||
|     'opacity_effect_with_target': OpacityEffectWithTargetGame.new, |     'opacity_effect_with_target': OpacityEffectWithTargetGame.new, | ||||||
|     'opacity_to_effect': OpacityToEffectGame.new, |     'opacity_to_effect': OpacityToEffectGame.new, | ||||||
|  |     'pointer_events': PointerEventsGame.new, | ||||||
|     'ray_cast': RayCastExample.new, |     'ray_cast': RayCastExample.new, | ||||||
|     'ray_trace': RayTraceExample.new, |     'ray_trace': RayTraceExample.new, | ||||||
|     'remove_effect': RemoveEffectGame.new, |     'remove_effect': RemoveEffectGame.new, | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								doc/flame/examples/lib/pointer_events.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								doc/flame/examples/lib/pointer_events.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | import 'dart:math'; | ||||||
|  |  | ||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flutter/rendering.dart'; | ||||||
|  |  | ||||||
|  | class PointerEventsGame extends FlameGame with TapCallbacks { | ||||||
|  |   @override | ||||||
|  |   Future<void> onLoad() async { | ||||||
|  |     add(HoverTarget(Vector2(100, 200))); | ||||||
|  |     add(HoverTarget(Vector2(300, 300))); | ||||||
|  |     add(HoverTarget(Vector2(400, 50))); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTapDown(TapDownEvent event) { | ||||||
|  |     add(HoverTarget(event.localPosition)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class HoverTarget extends PositionComponent with HoverCallbacks { | ||||||
|  |   static final Random _random = Random(); | ||||||
|  |  | ||||||
|  |   HoverTarget(Vector2 position) | ||||||
|  |       : super( | ||||||
|  |           position: position, | ||||||
|  |           size: Vector2.all(50), | ||||||
|  |           anchor: Anchor.center, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |   final _paint = Paint() | ||||||
|  |     ..color = HSLColor.fromAHSL(1, _random.nextDouble() * 360, 1, 0.8) | ||||||
|  |         .toColor() | ||||||
|  |         .withOpacity(0.5); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void render(Canvas canvas) { | ||||||
|  |     canvas.drawRect(size.toRect(), _paint); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onHoverEnter() { | ||||||
|  |     _paint.color = _paint.color.withOpacity(1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onHoverExit() { | ||||||
|  |     _paint.color = _paint.color.withOpacity(0.5); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								doc/flame/inputs/pointer_events.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								doc/flame/inputs/pointer_events.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | # Pointer Events | ||||||
|  |  | ||||||
|  | ```{note} | ||||||
|  | This document describes the new events API. The old (legacy) approach, | ||||||
|  | which is still supported, is described in [](gesture_input.md). | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Pointer events** are Flutter's generalized "mouse-movement"-type events (for desktop or web). | ||||||
|  |  | ||||||
|  | If you want to interact with mouse movement events within your component or game, you can use the | ||||||
|  | `PointerMoveCallbacks` mixin. | ||||||
|  |  | ||||||
|  | For example: | ||||||
|  |  | ||||||
|  | ```dart | ||||||
|  | class MyComponent extends PositionComponent with PointerMoveCallbacks { | ||||||
|  |   MyComponent() : super(size: Vector2(80, 60)); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onPointerMove(PointerMoveEvent event) { | ||||||
|  |     // Do something in response to the mouse move (e.g. update coordinates) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The mixin adds two overridable methods to your component: | ||||||
|  |  | ||||||
|  | - `onPointerMove`: called when the mouse moves within the component | ||||||
|  | - `onPointerMoveStop`: called once if the component was being hovered and the mouse leaves | ||||||
|  |  | ||||||
|  | By default, each of these methods does nothing, they need to be overridden in order to perform any | ||||||
|  | function. | ||||||
|  |  | ||||||
|  | In addition, the component must implement the `containsLocalPoint()` method (already implemented in | ||||||
|  | `PositionComponent`, so most of the time you don't need to do anything here) -- this method allows | ||||||
|  | Flame to know whether the event occurred within the component or not. | ||||||
|  |  | ||||||
|  | Note that only mouse events happening within your component will be proxied along. However, | ||||||
|  | `onPointerMoveStop` will be fired once on the first mouse movement that leaves your component, so | ||||||
|  | you can handle any exit conditions there. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## HoverCallbacks | ||||||
|  |  | ||||||
|  | If you want to specifically know if your component is being hovered or not, or if you want to hook | ||||||
|  | into hover enter and exist events, you can use a more dedicated mixin called `HoverCallbacks`. | ||||||
|  |  | ||||||
|  | For example: | ||||||
|  |  | ||||||
|  | ```dart | ||||||
|  | class MyComponent extends PositionComponent with HoverCallbacks { | ||||||
|  |  | ||||||
|  |   MyComponent() : super(size: Vector2(80, 60)); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void update(double dt) { | ||||||
|  |     // use `isHovered` to know if the component is being hovered | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onHoverEnter() { | ||||||
|  |     // Do something in response to the mouse entering the component | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onHoverExit() { | ||||||
|  |     // Do something in response to the mouse leaving the component | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Note that you can still listen to the "raw" onPointerMove methods for additional functionality, just | ||||||
|  | make sure to call the `super` version to enable the `HoverCallbacks` behavior. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Demo | ||||||
|  |  | ||||||
|  | Play with the demo below to see the pointer hover events in action. | ||||||
|  |  | ||||||
|  | ```{flutter-app} | ||||||
|  | :sources: ../flame/examples | ||||||
|  | :page: pointer_events | ||||||
|  | :show: widget code | ||||||
|  | ``` | ||||||
| @ -1,7 +1,7 @@ | |||||||
| # Tap Events | # Tap Events | ||||||
|  |  | ||||||
| ```{note} | ```{note} | ||||||
| This document describes the new tap events API. The old (legacy) approach, | This document describes the new events API. The old (legacy) approach, | ||||||
| which is still supported, is described in [](gesture_input.md). | which is still supported, is described in [](gesture_input.md). | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,37 +1,37 @@ | |||||||
| // ignore_for_file: deprecated_member_use |  | ||||||
| 
 |  | ||||||
| import 'package:flame/components.dart'; | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
| import 'package:flame/extensions.dart'; | import 'package:flame/extensions.dart'; | ||||||
| import 'package:flame/game.dart'; | import 'package:flame/game.dart'; | ||||||
| import 'package:flame/input.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| 
 | 
 | ||||||
| class HoverablesExample extends FlameGame with HasHoverables, TapDetector { | class HoverCallbacksExample extends FlameGame with TapCallbacks { | ||||||
|   static const String description = ''' |   static const String description = ''' | ||||||
|     This example shows how to use `Hoverable`s.\n\n |     This example shows how to use `HoverCallbacks`s.\n\n | ||||||
|     Add more squares by clicking and hover them to change their color. |     Add more squares by clicking and hover them to change their color. | ||||||
|   '''; |   '''; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<void> onLoad() async { |   Future<void> onLoad() async { | ||||||
|     add(HoverableSquare(Vector2(200, 500))); |     add(HoverSquare(Vector2(200, 500))); | ||||||
|     add(HoverableSquare(Vector2(700, 300))); |     add(HoverSquare(Vector2(700, 300))); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   void onTapDown(TapDownInfo info) { |   void onTapDown(TapDownEvent event) { | ||||||
|     add(HoverableSquare(info.eventPosition.game)); |     add(HoverSquare(event.localPosition)); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class HoverableSquare extends PositionComponent with Hoverable { | class HoverSquare extends PositionComponent with HoverCallbacks { | ||||||
|   static final Paint _white = Paint()..color = const Color(0xFFFFFFFF); |   static final Paint _white = Paint()..color = const Color(0xFFFFFFFF); | ||||||
|   static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5); |   static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5); | ||||||
| 
 | 
 | ||||||
|   HoverableSquare(Vector2 position) |   HoverSquare(Vector2 position) | ||||||
|       : super(position: position, size: Vector2.all(100)) { |       : super( | ||||||
|     anchor = Anchor.center; |           position: position, | ||||||
|   } |           size: Vector2.all(100), | ||||||
|  |           anchor: Anchor.center, | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   void render(Canvas canvas) { |   void render(Canvas canvas) { | ||||||
| @ -4,7 +4,7 @@ import 'package:examples/stories/input/double_tap_callbacks_example.dart'; | |||||||
| import 'package:examples/stories/input/draggables_example.dart'; | import 'package:examples/stories/input/draggables_example.dart'; | ||||||
| import 'package:examples/stories/input/gesture_hitboxes_example.dart'; | import 'package:examples/stories/input/gesture_hitboxes_example.dart'; | ||||||
| import 'package:examples/stories/input/hardware_keyboard_example.dart'; | import 'package:examples/stories/input/hardware_keyboard_example.dart'; | ||||||
| import 'package:examples/stories/input/hoverables_example.dart'; | import 'package:examples/stories/input/hover_callbacks_example.dart'; | ||||||
| import 'package:examples/stories/input/joystick_advanced_example.dart'; | import 'package:examples/stories/input/joystick_advanced_example.dart'; | ||||||
| import 'package:examples/stories/input/joystick_example.dart'; | import 'package:examples/stories/input/joystick_example.dart'; | ||||||
| import 'package:examples/stories/input/keyboard_example.dart'; | import 'package:examples/stories/input/keyboard_example.dart'; | ||||||
| @ -50,10 +50,10 @@ void addInputStories(Dashbook dashbook) { | |||||||
|       info: DoubleTapCallbacksExample.description, |       info: DoubleTapCallbacksExample.description, | ||||||
|     ) |     ) | ||||||
|     ..add( |     ..add( | ||||||
|       'Hoverables', |       'HoverCallbacks', | ||||||
|       (_) => GameWidget(game: HoverablesExample()), |       (_) => GameWidget(game: HoverCallbacksExample()), | ||||||
|       codeLink: baseLink('input/hoverables_example.dart'), |       codeLink: baseLink('input/hover_callbacks_example.dart'), | ||||||
|       info: HoverablesExample.description, |       info: HoverCallbacksExample.description, | ||||||
|     ) |     ) | ||||||
|     ..add( |     ..add( | ||||||
|       'Keyboard', |       'Keyboard', | ||||||
|  | |||||||
| @ -1,6 +1,9 @@ | |||||||
| export 'src/events/component_mixins/double_tap_callbacks.dart' | export 'src/events/component_mixins/double_tap_callbacks.dart' | ||||||
|     show DoubleTapCallbacks; |     show DoubleTapCallbacks; | ||||||
| export 'src/events/component_mixins/drag_callbacks.dart' show DragCallbacks; | export 'src/events/component_mixins/drag_callbacks.dart' show DragCallbacks; | ||||||
|  | export 'src/events/component_mixins/hover_callbacks.dart' show HoverCallbacks; | ||||||
|  | export 'src/events/component_mixins/pointer_move_callbacks.dart' | ||||||
|  |     show PointerMoveCallbacks; | ||||||
| export 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks; | export 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks; | ||||||
| export 'src/events/flame_game_mixins/has_draggables_bridge.dart' | export 'src/events/flame_game_mixins/has_draggables_bridge.dart' | ||||||
|     show HasDraggablesBridge; |     show HasDraggablesBridge; | ||||||
| @ -22,6 +25,7 @@ export 'src/events/messages/drag_cancel_event.dart' show DragCancelEvent; | |||||||
| export 'src/events/messages/drag_end_event.dart' show DragEndEvent; | export 'src/events/messages/drag_end_event.dart' show DragEndEvent; | ||||||
| export 'src/events/messages/drag_start_event.dart' show DragStartEvent; | export 'src/events/messages/drag_start_event.dart' show DragStartEvent; | ||||||
| export 'src/events/messages/drag_update_event.dart' show DragUpdateEvent; | export 'src/events/messages/drag_update_event.dart' show DragUpdateEvent; | ||||||
|  | export 'src/events/messages/pointer_move_event.dart' show PointerMoveEvent; | ||||||
| export 'src/events/messages/tap_cancel_event.dart' show TapCancelEvent; | export 'src/events/messages/tap_cancel_event.dart' show TapCancelEvent; | ||||||
| export 'src/events/messages/tap_down_event.dart' show TapDownEvent; | export 'src/events/messages/tap_down_event.dart' show TapDownEvent; | ||||||
| export 'src/events/messages/tap_up_event.dart' show TapUpEvent; | export 'src/events/messages/tap_up_event.dart' show TapUpEvent; | ||||||
|  | |||||||
| @ -0,0 +1,61 @@ | |||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/src/components/core/component.dart'; | ||||||
|  | import 'package:meta/meta.dart'; | ||||||
|  |  | ||||||
|  | /// This mixin can be added to a [Component] allowing it to receive hover | ||||||
|  | /// events. | ||||||
|  | /// | ||||||
|  | /// In addition to adding this mixin, the component must also implement the | ||||||
|  | /// [containsLocalPoint] method -- the component will only be considered | ||||||
|  | /// "hovered" if the point where the hover event occurred is inside the | ||||||
|  | /// component. | ||||||
|  | /// | ||||||
|  | /// This mixin is the replacement of the Hoverable mixin. | ||||||
|  | mixin HoverCallbacks on Component implements PointerMoveCallbacks { | ||||||
|  |   bool _isHovered = false; | ||||||
|  |  | ||||||
|  |   /// Returns true while the component is being dragged. | ||||||
|  |   bool get isHovered => _isHovered; | ||||||
|  |  | ||||||
|  |   void onHoverEnter() {} | ||||||
|  |  | ||||||
|  |   void onHoverExit() {} | ||||||
|  |  | ||||||
|  |   void _doHoverEnter() { | ||||||
|  |     _isHovered = true; | ||||||
|  |     onHoverEnter(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _doHoverExit() { | ||||||
|  |     _isHovered = false; | ||||||
|  |     onHoverExit(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onPointerMove(PointerMoveEvent event) { | ||||||
|  |     final position = event.localPosition; | ||||||
|  |     if (containsLocalPoint(position)) { | ||||||
|  |       if (!_isHovered) { | ||||||
|  |         _doHoverEnter(); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (_isHovered) { | ||||||
|  |         _doHoverExit(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onPointerMoveStop(PointerMoveEvent event) { | ||||||
|  |     if (_isHovered) { | ||||||
|  |       _doHoverExit(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   @mustCallSuper | ||||||
|  |   void onMount() { | ||||||
|  |     super.onMount(); | ||||||
|  |     PointerMoveCallbacks.onMountHandler(this); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/src/components/core/component.dart'; | ||||||
|  | import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart'; | ||||||
|  | import 'package:meta/meta.dart'; | ||||||
|  |  | ||||||
|  | /// This mixin can be added to a [Component] allowing it to receive | ||||||
|  | /// pointer movement events. | ||||||
|  | mixin PointerMoveCallbacks on Component { | ||||||
|  |   void onPointerMove(PointerMoveEvent event) {} | ||||||
|  |  | ||||||
|  |   void onPointerMoveStop(PointerMoveEvent event) {} | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   @mustCallSuper | ||||||
|  |   void onMount() { | ||||||
|  |     super.onMount(); | ||||||
|  |     onMountHandler(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static void onMountHandler(PointerMoveCallbacks instance) { | ||||||
|  |     final game = instance.findGame()!; | ||||||
|  |     const key = MouseMoveDispatcherKey(); | ||||||
|  |     if (game.findByKey(key) == null) { | ||||||
|  |       final dispatcher = PointerMoveDispatcher(); | ||||||
|  |       game.registerKey(key, dispatcher); | ||||||
|  |       game.add(dispatcher); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,71 @@ | |||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/src/components/core/component.dart'; | ||||||
|  | import 'package:flame/src/components/core/component_key.dart'; | ||||||
|  | import 'package:flame/src/events/tagged_component.dart'; | ||||||
|  | import 'package:flame/src/game/flame_game.dart'; | ||||||
|  | import 'package:flutter/gestures.dart' as flutter; | ||||||
|  | import 'package:meta/meta.dart'; | ||||||
|  |  | ||||||
|  | /// **MouseMoveDispatcher** facilitates dispatching of mouse move events to the | ||||||
|  | /// [PointerMoveCallbacks] components in the component tree. It will be attached | ||||||
|  | /// to the [FlameGame] instance automatically whenever any | ||||||
|  | /// [PointerMoveCallbacks] components are mounted into the component tree. | ||||||
|  | @internal | ||||||
|  | class PointerMoveDispatcher extends Component { | ||||||
|  |   /// The record of all components currently being hovered. | ||||||
|  |   final Set<TaggedComponent<PointerMoveCallbacks>> _records = {}; | ||||||
|  |  | ||||||
|  |   FlameGame get game => parent! as FlameGame; | ||||||
|  |  | ||||||
|  |   @mustCallSuper | ||||||
|  |   void onMouseMove(PointerMoveEvent event) { | ||||||
|  |     final updated = <TaggedComponent<PointerMoveCallbacks>>{}; | ||||||
|  |  | ||||||
|  |     event.deliverAtPoint( | ||||||
|  |       rootComponent: game, | ||||||
|  |       deliverToAll: true, | ||||||
|  |       eventHandler: (PointerMoveCallbacks component) { | ||||||
|  |         final tagged = TaggedComponent(event.pointerId, component); | ||||||
|  |         _records.add(tagged); | ||||||
|  |         updated.add(tagged); | ||||||
|  |         component.onPointerMove(event); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final toRemove = <TaggedComponent<PointerMoveCallbacks>>{}; | ||||||
|  |     for (final record in _records) { | ||||||
|  |       if (record.pointerId == event.pointerId && !updated.contains(record)) { | ||||||
|  |         // one last "exit" event | ||||||
|  |         record.component.onPointerMoveStop(event); | ||||||
|  |         toRemove.add(record); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     _records.removeAll(toRemove); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _handlePointerMove(flutter.PointerHoverEvent event) { | ||||||
|  |     onMouseMove(PointerMoveEvent.fromPointerHoverEvent(game, event)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onMount() { | ||||||
|  |     game.mouseDetector = _handlePointerMove; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onRemove() { | ||||||
|  |     game.mouseDetector = null; | ||||||
|  |     game.unregisterKey(const MouseMoveDispatcherKey()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class MouseMoveDispatcherKey implements ComponentKey { | ||||||
|  |   const MouseMoveDispatcherKey(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   int get hashCode => 'MouseMoveDispatcherKey'.hashCode; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   bool operator ==(dynamic other) => | ||||||
|  |       other is MouseMoveDispatcherKey && other.hashCode == hashCode; | ||||||
|  | } | ||||||
| @ -0,0 +1,44 @@ | |||||||
|  | import 'package:flame/extensions.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flame/src/events/messages/position_event.dart'; | ||||||
|  | import 'package:flutter/gestures.dart' as flutter; | ||||||
|  |  | ||||||
|  | class PointerMoveEvent extends PositionEvent { | ||||||
|  |   PointerMoveEvent( | ||||||
|  |     this.pointerId, | ||||||
|  |     super.game, | ||||||
|  |     flutter.PointerHoverEvent rawEvent, | ||||||
|  |   )   : timestamp = rawEvent.timeStamp, | ||||||
|  |         delta = rawEvent.delta.toVector2(), | ||||||
|  |         super( | ||||||
|  |           devicePosition: rawEvent.position.toVector2(), | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |   final int pointerId; | ||||||
|  |   final Duration timestamp; | ||||||
|  |   final Vector2 delta; | ||||||
|  |  | ||||||
|  |   static final _nanPoint = Vector2.all(double.nan); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Vector2 get localPosition { | ||||||
|  |     return renderingTrace.isEmpty ? _nanPoint : renderingTrace.last; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toString() => 'PointerMoveEvent(devicePosition: $devicePosition, ' | ||||||
|  |       'canvasPosition: $canvasPosition, ' | ||||||
|  |       'delta: $delta, ' | ||||||
|  |       'pointerId: $pointerId, timestamp: $timestamp)'; | ||||||
|  |  | ||||||
|  |   factory PointerMoveEvent.fromPointerHoverEvent( | ||||||
|  |     Game game, | ||||||
|  |     flutter.PointerHoverEvent event, | ||||||
|  |   ) { | ||||||
|  |     return PointerMoveEvent( | ||||||
|  |       event.pointer, | ||||||
|  |       game, | ||||||
|  |       event, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -8,6 +8,7 @@ import 'package:flame/src/game/game_render_box.dart'; | |||||||
| import 'package:flame/src/game/game_widget/gesture_detector_builder.dart'; | import 'package:flame/src/game/game_widget/gesture_detector_builder.dart'; | ||||||
| import 'package:flame/src/game/overlay_manager.dart'; | import 'package:flame/src/game/overlay_manager.dart'; | ||||||
| import 'package:flame/src/game/projector.dart'; | import 'package:flame/src/game/projector.dart'; | ||||||
|  | import 'package:flutter/gestures.dart'; | ||||||
| import 'package:flutter/rendering.dart'; | import 'package:flutter/rendering.dart'; | ||||||
| import 'package:flutter/widgets.dart'; | import 'package:flutter/widgets.dart'; | ||||||
| import 'package:meta/meta.dart'; | import 'package:meta/meta.dart'; | ||||||
| @ -35,6 +36,10 @@ abstract mixin class Game { | |||||||
|   late final GestureDetectorBuilder gestureDetectors = |   late final GestureDetectorBuilder gestureDetectors = | ||||||
|       GestureDetectorBuilder(refreshWidget)..initializeGestures(this); |       GestureDetectorBuilder(refreshWidget)..initializeGestures(this); | ||||||
|  |  | ||||||
|  |   /// Set by the PointerMoveDispatcher to receive mouse events from the | ||||||
|  |   /// game widget. | ||||||
|  |   void Function(PointerHoverEvent event)? mouseDetector; | ||||||
|  |  | ||||||
|   /// This should update the state of the game. |   /// This should update the state of the game. | ||||||
|   void update(double dt); |   void update(double dt); | ||||||
|  |  | ||||||
|  | |||||||
| @ -174,7 +174,8 @@ bool hasMouseDetectors(Game game) { | |||||||
|   return game is MouseMovementDetector || |   return game is MouseMovementDetector || | ||||||
|       game is ScrollDetector || |       game is ScrollDetector || | ||||||
|       // ignore: deprecated_member_use_from_same_package |       // ignore: deprecated_member_use_from_same_package | ||||||
|       game is HasHoverables; |       game is HasHoverables || | ||||||
|  |       game.mouseDetector != null; | ||||||
| } | } | ||||||
|  |  | ||||||
| Widget applyMouseDetectors(Game game, Widget child) { | Widget applyMouseDetectors(Game game, Widget child) { | ||||||
| @ -182,10 +183,14 @@ Widget applyMouseDetectors(Game game, Widget child) { | |||||||
|       ? game.onMouseMove |       ? game.onMouseMove | ||||||
|       // ignore: deprecated_member_use_from_same_package |       // ignore: deprecated_member_use_from_same_package | ||||||
|       : (game is HasHoverables ? game.onMouseMove : null); |       : (game is HasHoverables ? game.onMouseMove : null); | ||||||
|  |   final mouseDetector = game.mouseDetector; | ||||||
|   return Listener( |   return Listener( | ||||||
|     child: MouseRegion( |     child: MouseRegion( | ||||||
|       child: child, |       child: child, | ||||||
|       onHover: (e) => mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e)), |       onHover: (PointerHoverEvent e) { | ||||||
|  |         mouseMoveFn?.call(PointerHoverInfo.fromDetails(game, e)); | ||||||
|  |         mouseDetector?.call(e); | ||||||
|  |       }, | ||||||
|     ), |     ), | ||||||
|     onPointerSignal: (event) => |     onPointerSignal: (event) => | ||||||
|         game is ScrollDetector && event is PointerScrollEvent |         game is ScrollDetector && event is PointerScrollEvent | ||||||
|  | |||||||
| @ -0,0 +1,109 @@ | |||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart'; | ||||||
|  | import 'package:flame_test/flame_test.dart'; | ||||||
|  | import 'package:flutter_test/flutter_test.dart'; | ||||||
|  |  | ||||||
|  | void main() { | ||||||
|  |   group('HoverCallbacks', () { | ||||||
|  |     testWithFlameGame( | ||||||
|  |         'make sure HoverCallbacks components can be added to a FlameGame', | ||||||
|  |         (game) async { | ||||||
|  |       await game.ensureAdd(_HoverCallbacksComponent()); | ||||||
|  |       await game.ready(); | ||||||
|  |  | ||||||
|  |       _hasDispatcher(game); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     testWithFlameGame('receive hover events', (game) async { | ||||||
|  |       final component = _HoverCallbacksComponent( | ||||||
|  |         position: Vector2.all(10), | ||||||
|  |         size: Vector2.all(10), | ||||||
|  |       ); | ||||||
|  |       game.add(component); | ||||||
|  |       await game.ready(); | ||||||
|  |  | ||||||
|  |       _hasDispatcher(game); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(12)); | ||||||
|  |       component.checkHoverEventCounts(enter: 1, exit: 0); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(14)); | ||||||
|  |       component.checkHoverEventCounts(enter: 1, exit: 0); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(16)); | ||||||
|  |       component.checkHoverEventCounts(enter: 1, exit: 0); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(18)); | ||||||
|  |       component.checkHoverEventCounts(enter: 1, exit: 0); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(20)); | ||||||
|  |       component.checkHoverEventCounts(enter: 1, exit: 1); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(22)); | ||||||
|  |       component.checkHoverEventCounts(enter: 1, exit: 1); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(18)); | ||||||
|  |       component.checkHoverEventCounts(enter: 2, exit: 1); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(19)); | ||||||
|  |       component.checkHoverEventCounts(enter: 2, exit: 1); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(20)); | ||||||
|  |       component.checkHoverEventCounts(enter: 2, exit: 2); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void _mouseEvent(FlameGame game, Vector2 position) { | ||||||
|  |   game.firstChild<PointerMoveDispatcher>()!.onMouseMove( | ||||||
|  |         createMouseMoveEvent( | ||||||
|  |           game: game, | ||||||
|  |           position: position, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void _hasDispatcher(FlameGame game) { | ||||||
|  |   expect( | ||||||
|  |     game.children.whereType<PointerMoveDispatcher>(), | ||||||
|  |     hasLength(1), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | mixin _HoverInspector on HoverCallbacks { | ||||||
|  |   int hoverEnterEvent = 0; | ||||||
|  |   int hoverExitEvent = 0; | ||||||
|  |  | ||||||
|  |   void checkHoverEventCounts({required int enter, required int exit}) { | ||||||
|  |     expect( | ||||||
|  |       hoverEnterEvent, | ||||||
|  |       equals(enter), | ||||||
|  |       reason: 'Mismatched hover enter event count', | ||||||
|  |     ); | ||||||
|  |     expect( | ||||||
|  |       hoverExitEvent, | ||||||
|  |       equals(exit), | ||||||
|  |       reason: 'Mismatched hover exit event count', | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onHoverEnter() { | ||||||
|  |     hoverEnterEvent++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onHoverExit() { | ||||||
|  |     hoverExitEvent++; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _HoverCallbacksComponent extends PositionComponent | ||||||
|  |     with HoverCallbacks, _HoverInspector { | ||||||
|  |   _HoverCallbacksComponent({ | ||||||
|  |     super.position, | ||||||
|  |     super.size, | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -0,0 +1,113 @@ | |||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart'; | ||||||
|  | import 'package:flame_test/flame_test.dart'; | ||||||
|  | import 'package:flutter_test/flutter_test.dart'; | ||||||
|  |  | ||||||
|  | void main() { | ||||||
|  |   group('PointerMoveCallbacks', () { | ||||||
|  |     testWithFlameGame( | ||||||
|  |         'make sure PointerMoveCallbacks components can be added to a FlameGame', | ||||||
|  |         (game) async { | ||||||
|  |       await game.ensureAdd(_PointerMoveCallbacksComponent()); | ||||||
|  |       await game.ready(); | ||||||
|  |  | ||||||
|  |       _hasDispatcher(game); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     testWithFlameGame('receive pointer move events on component', (game) async { | ||||||
|  |       final c1 = _PointerMoveCallbacksComponent( | ||||||
|  |         position: Vector2.all(10), | ||||||
|  |         size: Vector2.all(10), | ||||||
|  |       ); | ||||||
|  |       game.add(c1); | ||||||
|  |       final c2 = _PointerMoveCallbacksComponent( | ||||||
|  |         position: Vector2.all(15), | ||||||
|  |         size: Vector2.all(10), | ||||||
|  |       ); | ||||||
|  |       game.add(c2); | ||||||
|  |  | ||||||
|  |       await game.ready(); | ||||||
|  |  | ||||||
|  |       _hasDispatcher(game); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(12)); | ||||||
|  |       expect(c1.removeSingle(), Vector2.all(2)); | ||||||
|  |       expect(c2.receivedEventsAt, isEmpty); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(1)); | ||||||
|  |       expect(c1.receivedEventsAt, isEmpty); | ||||||
|  |       expect(c2.receivedEventsAt, isEmpty); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(19)); | ||||||
|  |       expect(c1.removeSingle(), Vector2.all(9)); | ||||||
|  |       expect(c2.removeSingle(), Vector2.all(4)); | ||||||
|  |  | ||||||
|  |       _mouseEvent(game, Vector2.all(21)); | ||||||
|  |       expect(c1.receivedEventsAt, isEmpty); | ||||||
|  |       expect(c2.removeSingle(), Vector2.all(6)); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     testWithGame( | ||||||
|  |       'receive pointer move events on game', | ||||||
|  |       _PointerMoveCallbacksGame.new, | ||||||
|  |       (game) async { | ||||||
|  |         _hasDispatcher(game); | ||||||
|  |  | ||||||
|  |         _mouseEvent(game, Vector2.all(12)); | ||||||
|  |         expect(game.removeSingle(), Vector2.all(12)); | ||||||
|  |  | ||||||
|  |         _mouseEvent(game, Vector2.all(1)); | ||||||
|  |         expect(game.removeSingle(), Vector2.all(1)); | ||||||
|  |  | ||||||
|  |         _mouseEvent(game, Vector2.all(19)); | ||||||
|  |         expect(game.removeSingle(), Vector2.all(19)); | ||||||
|  |  | ||||||
|  |         _mouseEvent(game, Vector2.all(21)); | ||||||
|  |         expect(game.removeSingle(), Vector2.all(21)); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void _mouseEvent(FlameGame game, Vector2 position) { | ||||||
|  |   game.firstChild<PointerMoveDispatcher>()!.onMouseMove( | ||||||
|  |         createMouseMoveEvent( | ||||||
|  |           game: game, | ||||||
|  |           position: position, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void _hasDispatcher(FlameGame game) { | ||||||
|  |   expect( | ||||||
|  |     game.children.whereType<PointerMoveDispatcher>(), | ||||||
|  |     hasLength(1), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | mixin _PointerMoveInspector on PointerMoveCallbacks { | ||||||
|  |   List<Vector2> receivedEventsAt = []; | ||||||
|  |  | ||||||
|  |   Vector2 removeSingle() { | ||||||
|  |     expect(receivedEventsAt, hasLength(1)); | ||||||
|  |     return receivedEventsAt.removeAt(0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onPointerMove(PointerMoveEvent event) { | ||||||
|  |     receivedEventsAt.add(event.localPosition); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PointerMoveCallbacksComponent extends PositionComponent | ||||||
|  |     with PointerMoveCallbacks, _PointerMoveInspector { | ||||||
|  |   _PointerMoveCallbacksComponent({ | ||||||
|  |     super.position, | ||||||
|  |     super.size, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PointerMoveCallbacksGame extends FlameGame | ||||||
|  |     with PointerMoveCallbacks, _PointerMoveInspector {} | ||||||
| @ -6,6 +6,7 @@ export 'src/fails_assert.dart'; | |||||||
| export 'src/flame_test.dart'; | export 'src/flame_test.dart'; | ||||||
| export 'src/mock_gesture_events.dart'; | export 'src/mock_gesture_events.dart'; | ||||||
| export 'src/mock_image.dart'; | export 'src/mock_image.dart'; | ||||||
|  | export 'src/mock_pointer_move_event.dart'; | ||||||
| export 'src/mock_tap_drag_events.dart'; | export 'src/mock_tap_drag_events.dart'; | ||||||
| export 'src/random_test.dart'; | export 'src/random_test.dart'; | ||||||
| export 'src/test_flame_game.dart'; | export 'src/test_flame_game.dart'; | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								packages/flame_test/lib/src/mock_pointer_move_event.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/flame_test/lib/src/mock_pointer_move_event.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/extensions.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flutter/gestures.dart' as flutter; | ||||||
|  |  | ||||||
|  | PointerMoveEvent createMouseMoveEvent({ | ||||||
|  |   required Game game, | ||||||
|  |   int? pointerId, | ||||||
|  |   Vector2? position, | ||||||
|  |   Vector2? delta, | ||||||
|  |   Duration? timestamp, | ||||||
|  | }) { | ||||||
|  |   return PointerMoveEvent( | ||||||
|  |     pointerId ?? 1, | ||||||
|  |     game, | ||||||
|  |     flutter.PointerHoverEvent( | ||||||
|  |       timeStamp: timestamp ?? Duration.zero, | ||||||
|  |       position: position?.toOffset() ?? Offset.zero, | ||||||
|  |       delta: delta?.toOffset() ?? Offset.zero, | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Luan Nico
					Luan Nico