Skip to content

Commit

Permalink
feat: Add IgnoreEvents mixin to ignore events for the whole subtree (
Browse files Browse the repository at this point in the history
…#2811)

Currently the event system is quite inefficient when you have a lot of
components since it goes through the whole component tree, with this
mixin you can avoid traversing down sub trees that you know don't handle
any events.

I'll add docs as soon as #2809 is merged to avoid merge conflicts.
  • Loading branch information
spydon committed Oct 12, 2023
1 parent 3b0d7e6 commit 313411c
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 4 deletions.
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));
}

0 comments on commit 313411c

Please sign in to comment.