diff --git a/doc/flame/inputs/inputs.md b/doc/flame/inputs/inputs.md index 0cffbae2c2b..719277e7f89 100644 --- a/doc/flame/inputs/inputs.md +++ b/doc/flame/inputs/inputs.md @@ -1,10 +1,10 @@ # Inputs +- [Tap Events](tap_events.md) - [Drag Events](drag_events.md) - [Gesture Input](gesture_input.md) - [Keyboard Input](keyboard_input.md) -- [Other Inputs](other_inputs.md) -- [Tap Events](tap_events.md) +- [Other Inputs and Helpers](other_inputs.md) - [Hardware Keyboard Detector](hardware_keyboard_detector.md) ```{toctree} diff --git a/doc/flame/inputs/other_inputs.md b/doc/flame/inputs/other_inputs.md index 8f2427189cb..8599dbbd2c3 100644 --- a/doc/flame/inputs/other_inputs.md +++ b/doc/flame/inputs/other_inputs.md @@ -1,4 +1,4 @@ -# Other Inputs +# Other Inputs and Helpers This includes documentation for input methods besides keyboard and mouse. @@ -172,3 +172,14 @@ In addition to the already existing skins, the [ToggleButtonComponent] contains - `hoverAndSelectedSkin`: Hover on selectable and selected button (desktop and web). - `disabledAndSelectedSkin`: For when the button is selected and in the disabled state. - `defaultSelectedLabel`: Component shown on top of the skins when button is selected. + + +## IgnoreEvents mixin + +If you don't want a component subtree to receive events, you can use the `IgnoreEvents` mixin. +Once you have added this mixin you can turn off events to reach a component and its descendants by +setting `ignoreEvents = true` (default when the mixin is added), and then set it to `false` when you +want to receive events again. + +This can be done for optimization purposes, since all events currently go through the whole +component tree. diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index c5a3b1b0408..a92e40f9004 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -29,6 +29,7 @@ export 'src/components/mixins/has_paint.dart'; export 'src/components/mixins/has_time_scale.dart'; export 'src/components/mixins/has_visibility.dart'; export 'src/components/mixins/has_world.dart'; +export 'src/components/mixins/ignore_events.dart'; export 'src/components/mixins/keyboard_handler.dart'; export 'src/components/mixins/notifier.dart'; export 'src/components/mixins/parent_is_a.dart'; diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index 87182cde377..e382f4cb2f5 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -707,6 +707,9 @@ class Component { nestedPoints?.add(point); if (_children != null) { for (final child in _children!.reversed()) { + if (child is IgnoreEvents && child.ignoreEvents) { + continue; + } Vector2? childPoint = point; if (child is CoordinateTransform) { childPoint = (child as CoordinateTransform).parentToLocal(point); @@ -716,7 +719,9 @@ class Component { } } } - if (containsLocalPoint(point)) { + final shouldIgnoreEvents = + this is IgnoreEvents && (this as IgnoreEvents).ignoreEvents; + if (containsLocalPoint(point) && !shouldIgnoreEvents) { yield this; } nestedPoints?.removeLast(); diff --git a/packages/flame/lib/src/components/mixins/ignore_events.dart b/packages/flame/lib/src/components/mixins/ignore_events.dart new file mode 100644 index 00000000000..eb23abea36e --- /dev/null +++ b/packages/flame/lib/src/components/mixins/ignore_events.dart @@ -0,0 +1,15 @@ +import 'package:flame/src/components/core/component.dart'; + +/// This mixin allows a component and all it's descendants to ignore events. +/// +/// Do note that this will also ignore the component and its descendants in +/// calls to [Component.componentsAtPoint]. +/// +/// If you want to dynamically use this mixin, you can add it and set +/// [ignoreEvents] true or false at runtime. +/// +/// This mixin is to be used when you have a large subtree of components that +/// shouldn't receive any events and you want to optimize the event handling. +mixin IgnoreEvents on Component { + bool ignoreEvents = true; +} diff --git a/packages/flame/test/events/component_mixins/ignore_events_test.dart b/packages/flame/test/events/component_mixins/ignore_events_test.dart new file mode 100644 index 00000000000..f4c7c01821d --- /dev/null +++ b/packages/flame/test/events/component_mixins/ignore_events_test.dart @@ -0,0 +1,144 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('IgnoreEvents', () { + testWithFlameGame( + 'correctly ignores events all the way down the subtree', + (game) async { + final grandChild = _IgnoreTapCallbacksComponent(); + final child = _IgnoreTapCallbacksComponent(children: [grandChild]); + final component = _IgnoreTapCallbacksComponent( + position: Vector2.all(10), + children: [child], + ); + + await game.ensureAdd(component); + final dispatcher = game.firstChild()!; + + dispatcher.onTapDown( + createTapDownEvents( + game: game, + localPosition: const Offset(12, 12), + globalPosition: const Offset(12, 12), + ), + ); + expect(component.tapDownEvent, equals(0)); + expect(component.tapUpEvent, equals(0)); + expect(component.tapCancelEvent, equals(0)); + expect(child.tapDownEvent, equals(0)); + expect(child.tapUpEvent, equals(0)); + expect(child.tapCancelEvent, equals(0)); + expect(grandChild.tapDownEvent, equals(0)); + expect(grandChild.tapUpEvent, equals(0)); + expect(grandChild.tapCancelEvent, equals(0)); + + // [onTapUp] will call, if there was an [onTapDown] event before + dispatcher.onTapUp( + createTapUpEvents( + game: game, + localPosition: const Offset(12, 12), + globalPosition: const Offset(12, 12), + ), + ); + + expect(component.tapDownEvent, equals(0)); + expect(component.tapUpEvent, equals(0)); + expect(component.tapCancelEvent, equals(0)); + expect(child.tapDownEvent, equals(0)); + expect(child.tapUpEvent, equals(0)); + expect(child.tapCancelEvent, equals(0)); + expect(grandChild.tapDownEvent, equals(0)); + expect(grandChild.tapUpEvent, equals(0)); + expect(grandChild.tapCancelEvent, equals(0)); + }, + ); + + testWithFlameGame( + 'correctly accepts events all the way down the subtree when ignoreEvents ' + 'is false', + (game) async { + final grandChild = _IgnoreTapCallbacksComponent()..ignoreEvents = false; + final child = _IgnoreTapCallbacksComponent(children: [grandChild]) + ..ignoreEvents = false; + final component = _IgnoreTapCallbacksComponent( + position: Vector2.all(10), + children: [child], + )..ignoreEvents = false; + + await game.ensureAdd(component); + final dispatcher = game.firstChild()!; + + dispatcher.onTapDown( + createTapDownEvents( + game: game, + localPosition: const Offset(12, 12), + globalPosition: const Offset(12, 12), + ), + ); + expect(component.tapDownEvent, equals(1)); + expect(component.tapUpEvent, equals(0)); + expect(component.tapCancelEvent, equals(0)); + expect(child.tapDownEvent, equals(1)); + expect(child.tapUpEvent, equals(0)); + expect(child.tapCancelEvent, equals(0)); + expect(grandChild.tapDownEvent, equals(1)); + expect(grandChild.tapUpEvent, equals(0)); + expect(grandChild.tapCancelEvent, equals(0)); + + // [onTapUp] will call, if there was an [onTapDown] event before + dispatcher.onTapUp( + createTapUpEvents( + game: game, + localPosition: const Offset(12, 12), + globalPosition: const Offset(12, 12), + ), + ); + + expect(component.tapDownEvent, equals(1)); + expect(component.tapUpEvent, equals(1)); + expect(component.tapCancelEvent, equals(0)); + expect(child.tapDownEvent, equals(1)); + expect(child.tapUpEvent, equals(1)); + expect(child.tapCancelEvent, equals(0)); + expect(grandChild.tapDownEvent, equals(1)); + expect(grandChild.tapUpEvent, equals(1)); + expect(grandChild.tapCancelEvent, equals(0)); + }, + ); + }); +} + +mixin _TapCounter on TapCallbacks { + int tapDownEvent = 0; + int tapUpEvent = 0; + int longTapDownEvent = 0; + int tapCancelEvent = 0; + + @override + void onTapDown(TapDownEvent event) { + event.continuePropagation = true; + tapDownEvent++; + } + + @override + void onTapUp(TapUpEvent event) { + event.continuePropagation = true; + tapUpEvent++; + } + + @override + void onTapCancel(TapCancelEvent event) { + event.continuePropagation = true; + tapCancelEvent++; + } +} + +class _IgnoreTapCallbacksComponent extends PositionComponent + with TapCallbacks, _TapCounter, IgnoreEvents { + _IgnoreTapCallbacksComponent({super.position, super.children}) + : super(size: Vector2.all(10)); +}