Skip to content

Commit

Permalink
feat: Add HoverCallbacks (#2706)
Browse files Browse the repository at this point in the history
This creates HoverCallbacks (and PointerMoveCallbacks) to replicate the Hoverables behaviour in the new camera and event system.
  • Loading branch information
luanpotter committed Sep 10, 2023
1 parent 83f5ea4 commit d460b84
Show file tree
Hide file tree
Showing 18 changed files with 624 additions and 22 deletions.
1 change: 1 addition & 0 deletions .github/.cspell/gamedev_dictionary.txt
Expand Up @@ -156,3 +156,4 @@ viewports
vsync
widget's
unawaited
proxied
2 changes: 2 additions & 0 deletions doc/flame/examples/lib/main.dart
Expand Up @@ -18,6 +18,7 @@ import 'package:doc_flame_examples/move_to_effect.dart';
import 'package:doc_flame_examples/opacity_by_effect.dart';
import 'package:doc_flame_examples/opacity_effect_with_target.dart';
import 'package:doc_flame_examples/opacity_to_effect.dart';
import 'package:doc_flame_examples/pointer_events.dart';
import 'package:doc_flame_examples/ray_cast.dart';
import 'package:doc_flame_examples/ray_trace.dart';
import 'package:doc_flame_examples/remove_effect.dart';
Expand Down Expand Up @@ -60,6 +61,7 @@ void main() {
'opacity_by_effect': OpacityByEffectGame.new,
'opacity_effect_with_target': OpacityEffectWithTargetGame.new,
'opacity_to_effect': OpacityToEffectGame.new,
'pointer_events': PointerEventsGame.new,
'ray_cast': RayCastExample.new,
'ray_trace': RayTraceExample.new,
'remove_effect': RemoveEffectGame.new,
Expand Down
51 changes: 51 additions & 0 deletions doc/flame/examples/lib/pointer_events.dart
@@ -0,0 +1,51 @@
import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/rendering.dart';

class PointerEventsGame extends FlameGame with TapCallbacks {
@override
Future<void> onLoad() async {
add(HoverTarget(Vector2(100, 200)));
add(HoverTarget(Vector2(300, 300)));
add(HoverTarget(Vector2(400, 50)));
}

@override
void onTapDown(TapDownEvent event) {
add(HoverTarget(event.localPosition));
}
}

class HoverTarget extends PositionComponent with HoverCallbacks {
static final Random _random = Random();

HoverTarget(Vector2 position)
: super(
position: position,
size: Vector2.all(50),
anchor: Anchor.center,
);

final _paint = Paint()
..color = HSLColor.fromAHSL(1, _random.nextDouble() * 360, 1, 0.8)
.toColor()
.withOpacity(0.5);

@override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), _paint);
}

@override
void onHoverEnter() {
_paint.color = _paint.color.withOpacity(1);
}

@override
void onHoverExit() {
_paint.color = _paint.color.withOpacity(0.5);
}
}
84 changes: 84 additions & 0 deletions doc/flame/inputs/pointer_events.md
@@ -0,0 +1,84 @@
# Pointer Events

```{note}
This document describes the new events API. The old (legacy) approach,
which is still supported, is described in [](gesture_input.md).
```

**Pointer events** are Flutter's generalized "mouse-movement"-type events (for desktop or web).

If you want to interact with mouse movement events within your component or game, you can use the
`PointerMoveCallbacks` mixin.

For example:

```dart
class MyComponent extends PositionComponent with PointerMoveCallbacks {
MyComponent() : super(size: Vector2(80, 60));
@override
void onPointerMove(PointerMoveEvent event) {
// Do something in response to the mouse move (e.g. update coordinates)
}
}
```

The mixin adds two overridable methods to your component:

- `onPointerMove`: called when the mouse moves within the component
- `onPointerMoveStop`: called once if the component was being hovered and the mouse leaves

By default, each of these methods does nothing, they need to be overridden in order to perform any
function.

In addition, the component must implement the `containsLocalPoint()` method (already implemented in
`PositionComponent`, so most of the time you don't need to do anything here) -- this method allows
Flame to know whether the event occurred within the component or not.

Note that only mouse events happening within your component will be proxied along. However,
`onPointerMoveStop` will be fired once on the first mouse movement that leaves your component, so
you can handle any exit conditions there.


## HoverCallbacks

If you want to specifically know if your component is being hovered or not, or if you want to hook
into hover enter and exist events, you can use a more dedicated mixin called `HoverCallbacks`.

For example:

```dart
class MyComponent extends PositionComponent with HoverCallbacks {
MyComponent() : super(size: Vector2(80, 60));
@override
void update(double dt) {
// use `isHovered` to know if the component is being hovered
}
@override
void onHoverEnter() {
// Do something in response to the mouse entering the component
}
@override
void onHoverExit() {
// Do something in response to the mouse leaving the component
}
}
```

Note that you can still listen to the "raw" onPointerMove methods for additional functionality, just
make sure to call the `super` version to enable the `HoverCallbacks` behavior.


### Demo

Play with the demo below to see the pointer hover events in action.

```{flutter-app}
:sources: ../flame/examples
:page: pointer_events
:show: widget code
```
2 changes: 1 addition & 1 deletion doc/flame/inputs/tap_events.md
@@ -1,7 +1,7 @@
# Tap Events

```{note}
This document describes the new tap events API. The old (legacy) approach,
This document describes the new events API. The old (legacy) approach,
which is still supported, is described in [](gesture_input.md).
```

Expand Down
@@ -1,37 +1,37 @@
// ignore_for_file: deprecated_member_use

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';

class HoverablesExample extends FlameGame with HasHoverables, TapDetector {
class HoverCallbacksExample extends FlameGame with TapCallbacks {
static const String description = '''
This example shows how to use `Hoverable`s.\n\n
This example shows how to use `HoverCallbacks`s.\n\n
Add more squares by clicking and hover them to change their color.
''';

@override
Future<void> onLoad() async {
add(HoverableSquare(Vector2(200, 500)));
add(HoverableSquare(Vector2(700, 300)));
add(HoverSquare(Vector2(200, 500)));
add(HoverSquare(Vector2(700, 300)));
}

@override
void onTapDown(TapDownInfo info) {
add(HoverableSquare(info.eventPosition.game));
void onTapDown(TapDownEvent event) {
add(HoverSquare(event.localPosition));
}
}

class HoverableSquare extends PositionComponent with Hoverable {
class HoverSquare extends PositionComponent with HoverCallbacks {
static final Paint _white = Paint()..color = const Color(0xFFFFFFFF);
static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5);

HoverableSquare(Vector2 position)
: super(position: position, size: Vector2.all(100)) {
anchor = Anchor.center;
}
HoverSquare(Vector2 position)
: super(
position: position,
size: Vector2.all(100),
anchor: Anchor.center,
);

@override
void render(Canvas canvas) {
Expand Down
10 changes: 5 additions & 5 deletions examples/lib/stories/input/input.dart
Expand Up @@ -4,7 +4,7 @@ import 'package:examples/stories/input/double_tap_callbacks_example.dart';
import 'package:examples/stories/input/draggables_example.dart';
import 'package:examples/stories/input/gesture_hitboxes_example.dart';
import 'package:examples/stories/input/hardware_keyboard_example.dart';
import 'package:examples/stories/input/hoverables_example.dart';
import 'package:examples/stories/input/hover_callbacks_example.dart';
import 'package:examples/stories/input/joystick_advanced_example.dart';
import 'package:examples/stories/input/joystick_example.dart';
import 'package:examples/stories/input/keyboard_example.dart';
Expand Down Expand Up @@ -50,10 +50,10 @@ void addInputStories(Dashbook dashbook) {
info: DoubleTapCallbacksExample.description,
)
..add(
'Hoverables',
(_) => GameWidget(game: HoverablesExample()),
codeLink: baseLink('input/hoverables_example.dart'),
info: HoverablesExample.description,
'HoverCallbacks',
(_) => GameWidget(game: HoverCallbacksExample()),
codeLink: baseLink('input/hover_callbacks_example.dart'),
info: HoverCallbacksExample.description,
)
..add(
'Keyboard',
Expand Down
4 changes: 4 additions & 0 deletions packages/flame/lib/events.dart
@@ -1,6 +1,9 @@
export 'src/events/component_mixins/double_tap_callbacks.dart'
show DoubleTapCallbacks;
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/pointer_move_callbacks.dart'
show PointerMoveCallbacks;
export 'src/events/component_mixins/tap_callbacks.dart' show TapCallbacks;
export 'src/events/flame_game_mixins/has_draggables_bridge.dart'
show HasDraggablesBridge;
Expand All @@ -22,6 +25,7 @@ export 'src/events/messages/drag_cancel_event.dart' show DragCancelEvent;
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_update_event.dart' show DragUpdateEvent;
export 'src/events/messages/pointer_move_event.dart' show PointerMoveEvent;
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_up_event.dart' show TapUpEvent;
Expand Down
@@ -0,0 +1,61 @@
import 'package:flame/events.dart';
import 'package:flame/src/components/core/component.dart';
import 'package:meta/meta.dart';

/// This mixin can be added to a [Component] allowing it to receive hover
/// events.
///
/// In addition to adding this mixin, the component must also implement the
/// [containsLocalPoint] method -- the component will only be considered
/// "hovered" if the point where the hover event occurred is inside the
/// component.
///
/// This mixin is the replacement of the Hoverable mixin.
mixin HoverCallbacks on Component implements PointerMoveCallbacks {
bool _isHovered = false;

/// Returns true while the component is being dragged.
bool get isHovered => _isHovered;

void onHoverEnter() {}

void onHoverExit() {}

void _doHoverEnter() {
_isHovered = true;
onHoverEnter();
}

void _doHoverExit() {
_isHovered = false;
onHoverExit();
}

@override
void onPointerMove(PointerMoveEvent event) {
final position = event.localPosition;
if (containsLocalPoint(position)) {
if (!_isHovered) {
_doHoverEnter();
}
} else {
if (_isHovered) {
_doHoverExit();
}
}
}

@override
void onPointerMoveStop(PointerMoveEvent event) {
if (_isHovered) {
_doHoverExit();
}
}

@override
@mustCallSuper
void onMount() {
super.onMount();
PointerMoveCallbacks.onMountHandler(this);
}
}
@@ -0,0 +1,29 @@
import 'package:flame/events.dart';
import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/events/flame_game_mixins/pointer_move_dispatcher.dart';
import 'package:meta/meta.dart';

/// This mixin can be added to a [Component] allowing it to receive
/// pointer movement events.
mixin PointerMoveCallbacks on Component {
void onPointerMove(PointerMoveEvent event) {}

void onPointerMoveStop(PointerMoveEvent event) {}

@override
@mustCallSuper
void onMount() {
super.onMount();
onMountHandler(this);
}

static void onMountHandler(PointerMoveCallbacks instance) {
final game = instance.findGame()!;
const key = MouseMoveDispatcherKey();
if (game.findByKey(key) == null) {
final dispatcher = PointerMoveDispatcher();
game.registerKey(key, dispatcher);
game.add(dispatcher);
}
}
}

0 comments on commit d460b84

Please sign in to comment.