mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 17:06:50 +08:00 
			
		
		
		
	feat!: Support secondary taps (right click) on new callbacks system (#3741)
Support secondary taps (right click) on new callbacks system. In order to follow through with our [event system migration](https://docs.google.com/document/d/1nBUup9QCPioVwWL1zs79z1hWF882tAYfNywFMdye2Qc), we need to make sure the new system is equipped to support all use cases; also changes the existing TapCallbacks to be primary-only. I noticed that we don't support "secondary taps" (i.e. right clicks), so I am adding this. I honestly really dislike the fact that this is considered a completely different event from the left click, instead of just a property on the click event. But I kept this structure to replicate what Flutter does, so this is more familiar for users. I think that is worth the slight verbosity of having yet another detector. Also, it plays well this way with Flutter because that underlying events are a bit different (for example, the secondary ones don't support `pointId`). Note: this is a slight breaking change because the existing detector works for BOTH left and right click, but there is NO WAY of distinguishing them because the `buttons` property is not propagated in the Flutter end (massive oversight I believe - might put a PR later). Since this provides the secondary as a solution, it also removes secondary clicks from triggering the primary. I think this is more versatile than having tap detector=`(primary OR secondary)` and secondary=`(secondary only)`. I don't think this should affect basically any users because (1) desktop only and (2) this acceptance of right clicks was probably a bug anyway (for example, on the example it would rotate the square and also open the context menu, which is jarring). However I am happy to add an option or pursue a different approach, I believe this is the best path forward, IMO.
This commit is contained in:
		| @ -170,6 +170,34 @@ class MyComponent extends Component with TapCallbacks { | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### SecondaryTapCallbacks | ||||||
|  |  | ||||||
|  | In addition to the primary tap events (i.e. left mouse button on desktop), Flame also supports | ||||||
|  | secondary tap events (i.e. right mouse button on desktop). To receive these events, add the | ||||||
|  | `SecondaryTapCallbacks` mixin to your `PositionComponent`. | ||||||
|  |  | ||||||
|  | ```dart | ||||||
|  | class MyComponent extends PositionComponent with SecondaryTapCallbacks { | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapUp(SecondaryTapUpEvent event) { | ||||||
|  |     /// Do something | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapCancel(SecondaryTapCancelEvent event) { | ||||||
|  |     /// Do something | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapDown(SecondaryTapDownEvent event) { | ||||||
|  |     /// Do something | ||||||
|  |   } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | You can extend both `TapCallbacks` and `SecondaryTapCallbacks` in the same component to | ||||||
|  | receive both primary and secondary tap events. | ||||||
|  |  | ||||||
|  |  | ||||||
| ### DoubleTapCallbacks | ### DoubleTapCallbacks | ||||||
|  |  | ||||||
| Flame also offers a mixin named `DoubleTapCallbacks` to receive a double-tap event from the | Flame also offers a mixin named `DoubleTapCallbacks` to receive a double-tap event from the | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import 'package:examples/stories/input/multitap_advanced_example.dart'; | |||||||
| import 'package:examples/stories/input/multitap_example.dart'; | import 'package:examples/stories/input/multitap_example.dart'; | ||||||
| import 'package:examples/stories/input/overlapping_tap_callbacks_example.dart'; | import 'package:examples/stories/input/overlapping_tap_callbacks_example.dart'; | ||||||
| import 'package:examples/stories/input/scroll_example.dart'; | import 'package:examples/stories/input/scroll_example.dart'; | ||||||
|  | import 'package:examples/stories/input/secondary_tap_callbacks_example.dart'; | ||||||
| import 'package:examples/stories/input/tap_callbacks_example.dart'; | import 'package:examples/stories/input/tap_callbacks_example.dart'; | ||||||
| import 'package:flame/game.dart'; | import 'package:flame/game.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| @ -28,6 +29,12 @@ void addInputStories(Dashbook dashbook) { | |||||||
|       codeLink: baseLink('input/tap_callbacks_example.dart'), |       codeLink: baseLink('input/tap_callbacks_example.dart'), | ||||||
|       info: TapCallbacksExample.description, |       info: TapCallbacksExample.description, | ||||||
|     ) |     ) | ||||||
|  |     ..add( | ||||||
|  |       'SecondaryTapCallbacks', | ||||||
|  |       (_) => GameWidget(game: SecondaryTapCallbacksExample()), | ||||||
|  |       codeLink: baseLink('input/secondary_tap_callbacks_example.dart'), | ||||||
|  |       info: SecondaryTapCallbacksExample.description, | ||||||
|  |     ) | ||||||
|     ..add( |     ..add( | ||||||
|       'DragCallbacks', |       'DragCallbacks', | ||||||
|       (context) { |       (context) { | ||||||
|  | |||||||
| @ -0,0 +1,63 @@ | |||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/extensions.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flame/palette.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  |  | ||||||
|  | class SecondaryTapCallbacksExample extends FlameGame { | ||||||
|  |   static const String description = ''' | ||||||
|  |     In this example we show how to listen to both primary (left) and | ||||||
|  |     secondary (right) tap events using the `TapCallbacks` and | ||||||
|  |     the `SecondaryTapCallbacks` mixin to any `PositionComponent`.\n\n | ||||||
|  |     The squares will change color depending on which button was used to tap them. | ||||||
|  |   '''; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<void> onLoad() async { | ||||||
|  |     world.add(TappableSquare()..anchor = Anchor.center); | ||||||
|  |     world.add(TappableSquare()..y = 350); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class TappableSquare extends PositionComponent | ||||||
|  |     with TapCallbacks, SecondaryTapCallbacks { | ||||||
|  |   static final Paint _red = BasicPalette.red.paint(); | ||||||
|  |   static final Paint _blue = BasicPalette.blue.paint(); | ||||||
|  |   static final TextPaint _text = TextPaint( | ||||||
|  |     style: TextStyle(color: BasicPalette.white.color, fontSize: 24), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   int counter = 0; | ||||||
|  |   Paint _paint = _red; | ||||||
|  |  | ||||||
|  |   TappableSquare({Vector2? position}) | ||||||
|  |     : super( | ||||||
|  |         position: position ?? Vector2.all(100), | ||||||
|  |         size: Vector2.all(100), | ||||||
|  |         anchor: Anchor.center, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void render(Canvas canvas) { | ||||||
|  |     canvas.drawRect(size.toRect(), _paint); | ||||||
|  |     _text.render( | ||||||
|  |       canvas, | ||||||
|  |       '$counter', | ||||||
|  |       size / 2, | ||||||
|  |       anchor: Anchor.center, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTapDown(_) { | ||||||
|  |     _paint = _red; | ||||||
|  |     counter++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapDown(_) { | ||||||
|  |     _paint = _blue; | ||||||
|  |     counter++; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -4,6 +4,8 @@ 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/hover_callbacks.dart' show HoverCallbacks; | ||||||
| export 'src/events/component_mixins/pointer_move_callbacks.dart' | export 'src/events/component_mixins/pointer_move_callbacks.dart' | ||||||
|     show PointerMoveCallbacks; |     show PointerMoveCallbacks; | ||||||
|  | export 'src/events/component_mixins/secondary_tap_callbacks.dart' | ||||||
|  |     show SecondaryTapCallbacks; | ||||||
| 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/double_tap_dispatcher.dart' | export 'src/events/flame_game_mixins/double_tap_dispatcher.dart' | ||||||
|     show DoubleTapDispatcher, DoubleTapDispatcherKey; |     show DoubleTapDispatcher, DoubleTapDispatcherKey; | ||||||
| @ -13,6 +15,8 @@ export 'src/events/flame_game_mixins/multi_tap_dispatcher.dart' | |||||||
|     show MultiTapDispatcher, MultiTapDispatcherKey; |     show MultiTapDispatcher, MultiTapDispatcherKey; | ||||||
| export 'src/events/flame_game_mixins/pointer_move_dispatcher.dart' | export 'src/events/flame_game_mixins/pointer_move_dispatcher.dart' | ||||||
|     show PointerMoveDispatcher, MouseMoveDispatcherKey; |     show PointerMoveDispatcher, MouseMoveDispatcherKey; | ||||||
|  | export 'src/events/flame_game_mixins/secondary_tap_dispatcher.dart' | ||||||
|  |     show SecondaryTapDispatcher, SecondaryTapDispatcherKey; | ||||||
| export 'src/events/game_mixins/multi_touch_drag_detector.dart' | export 'src/events/game_mixins/multi_touch_drag_detector.dart' | ||||||
|     show MultiTouchDragDetector; |     show MultiTouchDragDetector; | ||||||
| export 'src/events/game_mixins/multi_touch_tap_detector.dart' | export 'src/events/game_mixins/multi_touch_tap_detector.dart' | ||||||
| @ -30,6 +34,12 @@ 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/pointer_move_event.dart' show PointerMoveEvent; | ||||||
|  | export 'src/events/messages/secondary_tap_cancel_event.dart' | ||||||
|  |     show SecondaryTapCancelEvent; | ||||||
|  | export 'src/events/messages/secondary_tap_down_event.dart' | ||||||
|  |     show SecondaryTapDownEvent; | ||||||
|  | export 'src/events/messages/secondary_tap_up_event.dart' | ||||||
|  |     show SecondaryTapUpEvent; | ||||||
| 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,30 @@ | |||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:meta/meta.dart'; | ||||||
|  |  | ||||||
|  | /// This mixin can be added to a [Component] allowing it to receive secondary | ||||||
|  | /// tap events (i.e. right mouse clicks). | ||||||
|  | /// | ||||||
|  | /// In addition to adding this mixin, the component must also implement the | ||||||
|  | /// [containsLocalPoint] method -- the component will only be considered | ||||||
|  | /// "tapped" if the point where the tap has occurred is inside the component. | ||||||
|  | /// | ||||||
|  | /// Note that FlameGame _is_ a [Component] and does implement | ||||||
|  | /// [containsLocalPoint]; so this can be used at the game level. | ||||||
|  | mixin SecondaryTapCallbacks on Component { | ||||||
|  |   void onSecondaryTapDown(SecondaryTapDownEvent event) {} | ||||||
|  |   void onSecondaryTapUp(SecondaryTapUpEvent event) {} | ||||||
|  |   void onSecondaryTapCancel(SecondaryTapCancelEvent event) {} | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   @mustCallSuper | ||||||
|  |   void onMount() { | ||||||
|  |     super.onMount(); | ||||||
|  |     final game = findRootGame()!; | ||||||
|  |     if (game.findByKey(const SecondaryTapDispatcherKey()) == null) { | ||||||
|  |       final dispatcher = SecondaryTapDispatcher(); | ||||||
|  |       game.registerKey(const SecondaryTapDispatcherKey(), dispatcher); | ||||||
|  |       game.add(dispatcher); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -154,7 +154,9 @@ class MultiTapDispatcher extends Component implements MultiTapListener { | |||||||
|   @override |   @override | ||||||
|   void onMount() { |   void onMount() { | ||||||
|     game.gestureDetectors.add<MultiTapGestureRecognizer>( |     game.gestureDetectors.add<MultiTapGestureRecognizer>( | ||||||
|       MultiTapGestureRecognizer.new, |       () => MultiTapGestureRecognizer( | ||||||
|  |         allowedButtonsFilter: (buttons) => buttons == kPrimaryButton, | ||||||
|  |       ), | ||||||
|       (MultiTapGestureRecognizer instance) { |       (MultiTapGestureRecognizer instance) { | ||||||
|         instance.longTapDelay = Duration( |         instance.longTapDelay = Duration( | ||||||
|           milliseconds: (longTapDelay * 1000).toInt(), |           milliseconds: (longTapDelay * 1000).toInt(), | ||||||
|  | |||||||
| @ -0,0 +1,69 @@ | |||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flutter/gestures.dart'; | ||||||
|  |  | ||||||
|  | class SecondaryTapDispatcherKey implements ComponentKey { | ||||||
|  |   const SecondaryTapDispatcherKey(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   int get hashCode => 'SecondaryTapDispatcherKey'.hashCode; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) => | ||||||
|  |       other is SecondaryTapDispatcherKey && other.hashCode == hashCode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// [SecondaryTapDispatcher] propagates secondary-tap events (i.e. right mouse | ||||||
|  | /// clicks) to every components in the component tree that is mixed with | ||||||
|  | /// [SecondaryTapCallbacks]. This will be attached to the [FlameGame] instance | ||||||
|  | /// automatically whenever any [SecondaryTapCallbacks] are mounted into the | ||||||
|  | /// component tree. | ||||||
|  | class SecondaryTapDispatcher extends Component | ||||||
|  |     with HasGameReference<FlameGame> { | ||||||
|  |   final _components = <SecondaryTapCallbacks>{}; | ||||||
|  |  | ||||||
|  |   void _onSecondaryTapDown(SecondaryTapDownEvent event) { | ||||||
|  |     event.deliverAtPoint( | ||||||
|  |       rootComponent: game, | ||||||
|  |       eventHandler: (SecondaryTapCallbacks component) { | ||||||
|  |         _components.add(component..onSecondaryTapDown(event)); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _onSecondaryTapUp(SecondaryTapUpEvent event) { | ||||||
|  |     for (final component in _components) { | ||||||
|  |       component.onSecondaryTapUp(event); | ||||||
|  |     } | ||||||
|  |     _components.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _onSecondaryTapCancel(SecondaryTapCancelEvent event) { | ||||||
|  |     for (final component in _components) { | ||||||
|  |       component.onSecondaryTapCancel(event); | ||||||
|  |     } | ||||||
|  |     _components.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onMount() { | ||||||
|  |     game.gestureDetectors.add( | ||||||
|  |       TapGestureRecognizer.new, | ||||||
|  |       (TapGestureRecognizer instance) { | ||||||
|  |         instance.onSecondaryTapDown = (details) => | ||||||
|  |             _onSecondaryTapDown(SecondaryTapDownEvent(game, details)); | ||||||
|  |         instance.onSecondaryTapCancel = () => | ||||||
|  |             _onSecondaryTapCancel(SecondaryTapCancelEvent()); | ||||||
|  |         instance.onSecondaryTapUp = (details) => | ||||||
|  |             _onSecondaryTapUp(SecondaryTapUpEvent(game, details)); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onRemove() { | ||||||
|  |     game.gestureDetectors.remove<TapGestureRecognizer>(); | ||||||
|  |     game.unregisterKey(const SecondaryTapDispatcherKey()); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | import 'package:flame/src/events/messages/event.dart'; | ||||||
|  | import 'package:flame/src/events/messages/secondary_tap_down_event.dart'; | ||||||
|  |  | ||||||
|  | /// The event propagated through the Flame engine when a secondary tap | ||||||
|  | /// (i.e. right mouse button click) on a component is cancelled. | ||||||
|  | /// | ||||||
|  | /// This event may occur for several reasons, such as: | ||||||
|  | ///  - a secondary tap was converted into a drag event (for a game where | ||||||
|  | ///    secondary drag events are enabled); | ||||||
|  | ///  - a secondary tap was cancelled on the game widget itself -- for example, | ||||||
|  | ///    if another app came into the foreground, or device turned off, etc; | ||||||
|  | ///  - a secondary tap was cancelled on a particular component because that | ||||||
|  | ///    component has moved away from the point of contact. | ||||||
|  | /// | ||||||
|  | /// The [SecondaryTapCancelEvent] will only occur if there was a previous | ||||||
|  | /// [SecondaryTapDownEvent]. | ||||||
|  | class SecondaryTapCancelEvent extends Event<void> { | ||||||
|  |   SecondaryTapCancelEvent() : super(raw: null); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toString() => 'SecondaryTapCancel()'; | ||||||
|  | } | ||||||
| @ -0,0 +1,28 @@ | |||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/extensions.dart'; | ||||||
|  | import 'package:flame/src/events/messages/position_event.dart'; | ||||||
|  | import 'package:flutter/gestures.dart'; | ||||||
|  |  | ||||||
|  | /// The event propagated through the Flame engine when the user starts a | ||||||
|  | /// secondary touch (i.e. right mouse click) on the game canvas. | ||||||
|  | /// | ||||||
|  | /// This is a [PositionEvent], where the position is the point of touch. | ||||||
|  | /// | ||||||
|  | /// In order for a component to be eligible to receive this event, it must add | ||||||
|  | /// the [SecondaryTapCallbacks] mixin. | ||||||
|  | class SecondaryTapDownEvent extends PositionEvent<TapDownDetails> { | ||||||
|  |   SecondaryTapDownEvent(super.game, TapDownDetails details) | ||||||
|  |     : deviceKind = details.kind ?? PointerDeviceKind.unknown, | ||||||
|  |       super( | ||||||
|  |         raw: details, | ||||||
|  |         devicePosition: details.globalPosition.toVector2(), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |   final PointerDeviceKind deviceKind; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toString() => | ||||||
|  |       'TapDownEvent(canvasPosition: $canvasPosition, ' | ||||||
|  |       'devicePosition: $devicePosition, ' | ||||||
|  |       'deviceKind: $deviceKind)'; | ||||||
|  | } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | import 'package:flame/extensions.dart'; | ||||||
|  | import 'package:flame/src/events/messages/position_event.dart'; | ||||||
|  | import 'package:flame/src/events/messages/secondary_tap_down_event.dart'; | ||||||
|  | import 'package:flutter/gestures.dart'; | ||||||
|  |  | ||||||
|  | /// The event propagated through the Flame engine when the user stops secondary | ||||||
|  | /// touching (i.e. right mouse button) the game canvas. | ||||||
|  | /// | ||||||
|  | /// This is a [PositionEvent], where the position is the point where the touch | ||||||
|  | /// has last occurred. | ||||||
|  | /// | ||||||
|  | /// The [SecondaryTapUpEvent] will only occur if there was a previous | ||||||
|  | /// [SecondaryTapDownEvent]. | ||||||
|  | class SecondaryTapUpEvent extends PositionEvent<TapUpDetails> { | ||||||
|  |   SecondaryTapUpEvent(super.game, TapUpDetails details) | ||||||
|  |     : deviceKind = details.kind, | ||||||
|  |       super( | ||||||
|  |         raw: details, | ||||||
|  |         devicePosition: details.globalPosition.toVector2(), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |   final PointerDeviceKind deviceKind; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String toString() => | ||||||
|  |       'SecondaryTapUpEvent(canvasPosition: $canvasPosition, ' | ||||||
|  |       'devicePosition: $devicePosition, ' | ||||||
|  |       'deviceKind: $deviceKind)'; | ||||||
|  | } | ||||||
| @ -0,0 +1,272 @@ | |||||||
|  | import 'package:flame/components.dart'; | ||||||
|  | import 'package:flame/events.dart'; | ||||||
|  | import 'package:flame/game.dart'; | ||||||
|  | import 'package:flame_test/flame_test.dart'; | ||||||
|  | import 'package:flutter/gestures.dart'; | ||||||
|  | import 'package:flutter_test/flutter_test.dart'; | ||||||
|  |  | ||||||
|  | void main() { | ||||||
|  |   group('SecondaryTapCallbacks', () { | ||||||
|  |     testWidgets( | ||||||
|  |       'receives secondary tap event', | ||||||
|  |       (tester) async { | ||||||
|  |         final component = _SecondaryTapCallbacksComponent() | ||||||
|  |           ..position = Vector2.all(10); | ||||||
|  |         await tester.pumpWidget( | ||||||
|  |           GameWidget( | ||||||
|  |             game: FlameGame(children: [component]), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |         await tester.pump(); | ||||||
|  |  | ||||||
|  |         final gesture = await tester.createGesture( | ||||||
|  |           buttons: kSecondaryButton, | ||||||
|  |         ); | ||||||
|  |         await gesture.down(const Offset(10, 10)); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapCancel, 0); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await gesture.up(); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapCancel, 0); | ||||||
|  |         expect(component.secondaryTap, 1); | ||||||
|  |  | ||||||
|  |         await tester.pump(kDoubleTapMinTime); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     testWidgets( | ||||||
|  |       'primary and secondary are received separately', | ||||||
|  |       (tester) async { | ||||||
|  |         final component = _BothTapCallbacksComponent() | ||||||
|  |           ..position = Vector2.all(10); | ||||||
|  |         await tester.pumpWidget( | ||||||
|  |           GameWidget( | ||||||
|  |             game: FlameGame(children: [component]), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |         await tester.pump(); | ||||||
|  |  | ||||||
|  |         final primaryGesture = await tester.createGesture(); | ||||||
|  |         final secondaryGesture = await tester.createGesture( | ||||||
|  |           buttons: kSecondaryButton, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         await primaryGesture.down(const Offset(10, 10)); | ||||||
|  |         expect(component.primaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapDown, 0); | ||||||
|  |  | ||||||
|  |         await primaryGesture.up(); | ||||||
|  |         expect(component.primaryTapUp, 1); | ||||||
|  |         expect(component.secondaryTapUp, 0); | ||||||
|  |  | ||||||
|  |         await secondaryGesture.down(const Offset(10, 10)); | ||||||
|  |         expect(component.primaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |  | ||||||
|  |         await secondaryGesture.up(); | ||||||
|  |         expect(component.primaryTapUp, 1); | ||||||
|  |         expect(component.secondaryTapUp, 1); | ||||||
|  |  | ||||||
|  |         await primaryGesture.down(const Offset(10, 10)); | ||||||
|  |         expect(component.primaryTapDown, 2); | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |  | ||||||
|  |         await secondaryGesture.down(const Offset(10, 10)); | ||||||
|  |         expect(component.primaryTapDown, 2); | ||||||
|  |         expect(component.secondaryTapDown, 2); | ||||||
|  |  | ||||||
|  |         await primaryGesture.up(); | ||||||
|  |         expect(component.primaryTapUp, 2); | ||||||
|  |         expect(component.secondaryTapUp, 1); | ||||||
|  |  | ||||||
|  |         await secondaryGesture.up(); | ||||||
|  |         expect(component.primaryTapUp, 2); | ||||||
|  |         expect(component.secondaryTapUp, 2); | ||||||
|  |  | ||||||
|  |         await tester.pump(kDoubleTapMinTime); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     testWidgets( | ||||||
|  |       '''does not receive an event when secondary-tapping a position far from the component''', | ||||||
|  |       (tester) async { | ||||||
|  |         final component = _SecondaryTapCallbacksComponent() | ||||||
|  |           ..position = Vector2.all(10); | ||||||
|  |         await tester.pumpWidget( | ||||||
|  |           GameWidget( | ||||||
|  |             game: FlameGame(children: [component]), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |         await tester.pump(); | ||||||
|  |         await tester.pump(); | ||||||
|  |  | ||||||
|  |         final gesture = await tester.createGesture(); | ||||||
|  |         await gesture.down(const Offset(100, 100)); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 0); | ||||||
|  |         expect(component.secondaryTapCancel, 0); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await gesture.up(); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 0); | ||||||
|  |         expect(component.secondaryTapCancel, 0); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await tester.pump(kDoubleTapMinTime); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     testWidgets( | ||||||
|  |       'receives a cancel event when gesture is canceled by drag', | ||||||
|  |       (tester) async { | ||||||
|  |         final component = _SecondaryTapCallbacksComponent() | ||||||
|  |           ..position = Vector2.all(10); | ||||||
|  |         await tester.pumpWidget( | ||||||
|  |           GameWidget( | ||||||
|  |             game: FlameGame(children: [component]), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |         await tester.pump(); | ||||||
|  |         await tester.pump(); | ||||||
|  |  | ||||||
|  |         final gesture = await tester.createGesture( | ||||||
|  |           buttons: kSecondaryButton, | ||||||
|  |         ); | ||||||
|  |         await gesture.down(const Offset(10, 10)); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapCancel, 0); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await gesture.moveBy(const Offset(100, 100)); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapCancel, 1); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await tester.pump(kDoubleTapMinTime); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     testWidgets( | ||||||
|  |       'receives a cancel event when gesture is canceled by cancel', | ||||||
|  |       (tester) async { | ||||||
|  |         final component = _SecondaryTapCallbacksComponent() | ||||||
|  |           ..position = Vector2.all(10); | ||||||
|  |         await tester.pumpWidget( | ||||||
|  |           GameWidget( | ||||||
|  |             game: FlameGame(children: [component]), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |         await tester.pump(); | ||||||
|  |         await tester.pump(); | ||||||
|  |  | ||||||
|  |         final gesture = await tester.createGesture( | ||||||
|  |           buttons: kSecondaryButton, | ||||||
|  |         ); | ||||||
|  |         await gesture.down(const Offset(10, 10)); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapCancel, 0); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await gesture.cancel(); | ||||||
|  |  | ||||||
|  |         expect(component.secondaryTapDown, 1); | ||||||
|  |         expect(component.secondaryTapCancel, 1); | ||||||
|  |         expect(component.secondaryTap, 0); | ||||||
|  |  | ||||||
|  |         await tester.pump(kDoubleTapMinTime); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     testWithFlameGame( | ||||||
|  |       'SecondaryTapDispatcher is added to game when the callback is mounted', | ||||||
|  |       (game) async { | ||||||
|  |         final component = _SecondaryTapCallbacksComponent(); | ||||||
|  |         await game.add(component); | ||||||
|  |         await game.ready(); | ||||||
|  |  | ||||||
|  |         expect(game.firstChild<SecondaryTapDispatcher>(), isNotNull); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _SecondaryTapCallbacksComponent extends PositionComponent | ||||||
|  |     with SecondaryTapCallbacks { | ||||||
|  |   _SecondaryTapCallbacksComponent() { | ||||||
|  |     anchor = Anchor.center; | ||||||
|  |     size = Vector2.all(10); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int secondaryTapDown = 0; | ||||||
|  |   int secondaryTapCancel = 0; | ||||||
|  |   int secondaryTap = 0; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapUp(SecondaryTapUpEvent event) { | ||||||
|  |     secondaryTap++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapCancel(SecondaryTapCancelEvent event) { | ||||||
|  |     secondaryTapCancel++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapDown(SecondaryTapDownEvent event) { | ||||||
|  |     secondaryTapDown++; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _BothTapCallbacksComponent extends PositionComponent | ||||||
|  |     with TapCallbacks, SecondaryTapCallbacks { | ||||||
|  |   _BothTapCallbacksComponent() { | ||||||
|  |     anchor = Anchor.center; | ||||||
|  |     size = Vector2.all(10); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int primaryTapDown = 0; | ||||||
|  |   int primaryTapUp = 0; | ||||||
|  |   int primaryTapCancel = 0; | ||||||
|  |  | ||||||
|  |   int secondaryTapDown = 0; | ||||||
|  |   int secondaryTapUp = 0; | ||||||
|  |   int secondaryTapCancel = 0; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTapUp(TapUpEvent event) { | ||||||
|  |     primaryTapUp++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTapCancel(TapCancelEvent event) { | ||||||
|  |     primaryTapCancel++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTapDown(TapDownEvent event) { | ||||||
|  |     primaryTapDown++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapUp(SecondaryTapUpEvent event) { | ||||||
|  |     secondaryTapUp++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapCancel(SecondaryTapCancelEvent event) { | ||||||
|  |     secondaryTapCancel++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onSecondaryTapDown(SecondaryTapDownEvent event) { | ||||||
|  |     secondaryTapDown++; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -38,6 +38,38 @@ TapUpEvent createTapUpEvents({ | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | SecondaryTapDownEvent createSecondaryTapDownEvents({ | ||||||
|  |   required Game game, | ||||||
|  |   PointerDeviceKind? kind, | ||||||
|  |   Offset? globalPosition, | ||||||
|  |   Offset? localPosition, | ||||||
|  | }) { | ||||||
|  |   return SecondaryTapDownEvent( | ||||||
|  |     game, | ||||||
|  |     TapDownDetails( | ||||||
|  |       localPosition: localPosition ?? Offset.zero, | ||||||
|  |       globalPosition: globalPosition ?? Offset.zero, | ||||||
|  |       kind: kind ?? PointerDeviceKind.touch, | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SecondaryTapUpEvent createSecondaryTapUpEvents({ | ||||||
|  |   required Game game, | ||||||
|  |   PointerDeviceKind? kind, | ||||||
|  |   Offset? globalPosition, | ||||||
|  |   Offset? localPosition, | ||||||
|  | }) { | ||||||
|  |   return SecondaryTapUpEvent( | ||||||
|  |     game, | ||||||
|  |     TapUpDetails( | ||||||
|  |       localPosition: localPosition ?? Offset.zero, | ||||||
|  |       globalPosition: globalPosition ?? Offset.zero, | ||||||
|  |       kind: kind ?? PointerDeviceKind.touch, | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
| DragStartEvent createDragStartEvents({ | DragStartEvent createDragStartEvents({ | ||||||
|   required Game game, |   required Game game, | ||||||
|   int? pointerId, |   int? pointerId, | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Luan Nico
					Luan Nico