Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add IgnoreEvents mixin to ignore events for the whole subtree #2811

Merged
merged 6 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/flame/inputs/inputs.md
Original file line number Diff line number Diff line change
@@ -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}
Expand Down
13 changes: 12 additions & 1 deletion doc/flame/inputs/other_inputs.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Other Inputs
# Other Inputs and Helpers

This includes documentation for input methods besides keyboard and mouse.

Expand Down Expand Up @@ -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.
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 6 additions & 1 deletion packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down
15 changes: 15 additions & 0 deletions packages/flame/lib/src/components/mixins/ignore_events.dart
Original file line number Diff line number Diff line change
@@ -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;
}
144 changes: 144 additions & 0 deletions packages/flame/test/events/component_mixins/ignore_events_test.dart
Original file line number Diff line number Diff line change
@@ -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<MultiTapDispatcher>()!;

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<MultiTapDispatcher>()!;

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));
}