Skip to content
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
7 changes: 4 additions & 3 deletions doc/flame/game_widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ instead.
```{note}
When using `deferToChild` or `translucent`, `FlameGame` determines whether a
position has an interactive component by traversing the component tree via
`componentsAtPoint`. Games that directly extend the low-level `Game` class
report a hit on their entire surface by default; override
`containsEventHandlerAt` to customize this.
`componentsAtPoint`, and treating any component that implements
`PointerInputCallbacks` as interactive. Games that directly extend the
low-level `Game` class report a hit on their entire surface by default;
override `containsEventHandlerAt` to customize this.
```
3 changes: 3 additions & 0 deletions packages/flame/lib/events.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export 'src/events/callbacks/double_tap_callbacks.dart' show DoubleTapCallbacks;
export 'src/events/callbacks/drag_callbacks.dart' show DragCallbacks;
export 'src/events/callbacks/hover_callbacks.dart' show HoverCallbacks;
export 'src/events/callbacks/input_callbacks.dart' show InputCallbacks;
export 'src/events/callbacks/long_press_callbacks.dart' show LongPressCallbacks;
export 'src/events/callbacks/pointer_input_callbacks.dart'
show PointerInputCallbacks;
export 'src/events/callbacks/pointer_move_callbacks.dart'
show PointerMoveCallbacks;
export 'src/events/callbacks/scale_callbacks.dart' show ScaleCallbacks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:flame/events.dart';
/// far away from each other, only one callback will be fired (or none).
///
/// This callback uses [DoubleTapDispatcher] to route events.
mixin DoubleTapCallbacks on Component {
mixin DoubleTapCallbacks on Component implements PointerInputCallbacks {
/// This triggers when the pointer stops contacting the device after the
/// second tap.
void onDoubleTapUp(DoubleTapEvent event) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:meta/meta.dart';
/// This mixin is the replacement of the Draggable mixin.
///
/// This callback uses [MultiDragScaleDispatcher] to route events.
mixin DragCallbacks on Component {
mixin DragCallbacks on Component implements PointerInputCallbacks {
bool _isDragged = false;

/// Returns true while the component is being dragged.
Expand Down
4 changes: 4 additions & 0 deletions packages/flame/lib/src/events/callbacks/input_callbacks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Marker interface implemented by every input callbacks mixin.
///
/// See `PointerInputCallbacks` for the positional subset.
abstract interface class InputCallbacks {}
Comment thread
luanpotter marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this and the other interface define methods?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they don't actually share any methods or fields atm - this is just a marker interface for type-checking purposes

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import 'package:flutter/foundation.dart';
/// - [onLongPressCancel]: called if the gesture is cancelled before completion.
///
/// This callback uses [LongPressDispatcher] to route events.
mixin LongPressCallbacks on Component {
mixin LongPressCallbacks on Component implements PointerInputCallbacks {
bool _isLongPressing = false;

/// Returns true while a long press gesture is active on this component.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:flame/src/events/callbacks/input_callbacks.dart';

/// Marker interface implemented by every input callbacks mixin whose events
/// carry a position (taps, drags, scrolls, hover) and thus participate in
/// hit-testing.
Comment thread
luanpotter marked this conversation as resolved.
///
/// Non-positional input, such as keyboard, implements [InputCallbacks].
abstract interface class PointerInputCallbacks implements InputCallbacks {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:meta/meta.dart';
/// pointer movement events.
///
/// This callback uses [PointerMoveDispatcher] to route events.
mixin PointerMoveCallbacks on Component {
mixin PointerMoveCallbacks on Component implements PointerInputCallbacks {
void onPointerMove(PointerMoveEvent event) {}

void onPointerMoveStop(PointerMoveEvent event) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:flame/events.dart';
import 'package:flutter/foundation.dart';

/// Mixin for components that respond to scale (pinch/zoom/rotate) gestures.
mixin ScaleCallbacks on Component {
mixin ScaleCallbacks on Component implements PointerInputCallbacks {
bool _isScaling = false;

/// Returns true while the component is being scaled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:meta/meta.dart';
/// pointer scroll (mouse wheel) events.
///
/// This callback uses [ScrollDispatcher] to route events.
mixin ScrollCallbacks on Component {
mixin ScrollCallbacks on Component implements PointerInputCallbacks {
void onScroll(ScrollEvent event) {}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'package:meta/meta.dart';
/// [containsLocalPoint]; so this can be used at the game level.
///
/// This callback uses [NonPrimaryTapDispatcher] to route events.
mixin SecondaryTapCallbacks on Component {
mixin SecondaryTapCallbacks on Component implements PointerInputCallbacks {
void onSecondaryTapDown(SecondaryTapDownEvent event) {}
void onSecondaryTapUp(SecondaryTapUpEvent event) {}
void onSecondaryTapCancel(SecondaryTapCancelEvent event) {}
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/src/events/callbacks/tap_callbacks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:meta/meta.dart';
/// [containsLocalPoint]; so this can be used at the game level.
///
/// This callback uses [MultiTapDispatcher] to route events.
mixin TapCallbacks on Component {
mixin TapCallbacks on Component implements PointerInputCallbacks {
void onTapDown(TapDownEvent event) {}
void onLongTapDown(TapDownEvent event) {}
void onTapUp(TapUpEvent event) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'package:meta/meta.dart';
/// [containsLocalPoint]; so this can be used at the game level.
///
/// This callback uses [NonPrimaryTapDispatcher] to route events.
mixin TertiaryTapCallbacks on Component {
mixin TertiaryTapCallbacks on Component implements PointerInputCallbacks {
void onTertiaryTapDown(TertiaryTapDownEvent event) {}
void onTertiaryTapUp(TertiaryTapUpEvent event) {}
void onTertiaryTapCancel(TertiaryTapCancelEvent event) {}
Expand Down
6 changes: 1 addition & 5 deletions packages/flame/lib/src/game/flame_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,7 @@ class FlameGame<W extends World> extends ComponentTreeRoot
return true;
}
for (final component in super.componentsAtPoint(position)) {
if (component is TapCallbacks ||
component is DragCallbacks ||
component is DoubleTapCallbacks ||
component is ScaleCallbacks ||
component is SecondaryTapCallbacks) {
if (component is PointerInputCallbacks) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the fix

return true;
}
}
Expand Down
126 changes: 126 additions & 0 deletions packages/flame/test/game/contains_event_handler_at_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('FlameGame.containsEventHandlerAt', () {
testWithGame(
'detects any component implementing the marker',
FlameGame.new,
(game) async {
await game.ensureAdd(_CustomInputComponent());

expect(game.containsEventHandlerAt(Vector2(30, 30)), isTrue);
expect(game.containsEventHandlerAt(Vector2(400, 300)), isFalse);
},
);

testWithGame(
'ignores components that handle no input',
FlameGame.new,
(game) async {
await game.ensureAdd(_PlainComponent());

expect(game.containsEventHandlerAt(Vector2(30, 30)), isFalse);
},
);

testWithGame(
'detects a built-in mixin',
FlameGame.new,
(game) async {
await game.ensureAdd(_ScrollComponent());

expect(game.containsEventHandlerAt(Vector2(30, 30)), isTrue);
},
);

testWithGame(
'detects a game that handles input itself',
_ScrollGame.new,
(game) async {
// FlameGame is itself a Component, so componentsAtPoint yields the
// game and any in-bounds point counts as a hit.
expect(game.containsEventHandlerAt(Vector2(400, 300)), isTrue);
expect(game.containsEventHandlerAt(Vector2(900, 700)), isFalse);
},
);
});

group('GameWidget hit test', () {
testWidgets(
'long presses reach a LongPressCallbacks component under deferToChild',
(tester) async {
var buttonTapped = false;
final component = _LongPressComponent()
..size = Vector2(800, 600)
..position = Vector2.zero();
final game = _TransparentGame()..add(component);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Stack(
children: [
Center(
child: ElevatedButton(
onPressed: () => buttonTapped = true,
child: const Text('Tap me'),
),
),
Positioned.fill(
child: GameWidget(
game: game,
behavior: HitTestBehavior.deferToChild,
),
),
],
),
),
),
);
await tester.pump();
await tester.pump();
expect(component.isMounted, isTrue);

await tester.longPressAt(const Offset(400, 300));
await tester.pump(const Duration(milliseconds: 100));

expect(component.longPressCount, equals(1));
expect(buttonTapped, isFalse);
},
);
});
}

mixin _CustomInputCallbacks on Component implements PointerInputCallbacks {}

class _TransparentGame extends FlameGame {
@override
Color backgroundColor() => const Color(0x00000000);
}

class _ScrollGame extends FlameGame with ScrollCallbacks {}

class _Box extends PositionComponent {
_Box() : super(position: Vector2.all(10), size: Vector2.all(50));
}

class _PlainComponent extends _Box {}

class _CustomInputComponent extends _Box with _CustomInputCallbacks {}

class _ScrollComponent extends _Box with ScrollCallbacks {}

class _LongPressComponent extends _Box with LongPressCallbacks {
int longPressCount = 0;

@override
void onLongPressStart(LongPressStartEvent event) {
super.onLongPressStart(event);
longPressCount++;
}
}
Loading