mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 16:36:57 +08:00
Adding custom mouse cursors for flame (#935)
* Adding custom mouse cursors for flame * linting and adding fvm to gitignore * PR suggestions * Apply suggestions from code review Co-authored-by: Luan Nico <luanpotter27@gmail.com> Co-authored-by: Luan Nico <luanpotter27@gmail.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,3 +18,5 @@ desktop/
|
|||||||
build/
|
build/
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
.fvm
|
||||||
|
|||||||
@ -102,6 +102,22 @@ and [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-cla
|
|||||||
also read more about Flutter's gestures
|
also read more about Flutter's gestures
|
||||||
[here](https://api.flutter.dev/flutter/gestures/gestures-library.html).
|
[here](https://api.flutter.dev/flutter/gestures/gestures-library.html).
|
||||||
|
|
||||||
|
It is also possible to change the current mouse cursor displayed on the `GameWidget` region. To do
|
||||||
|
so the following code can be used inside the `Game` class
|
||||||
|
|
||||||
|
```dart
|
||||||
|
mouseCursor.value = SystemMouseCursors.move;
|
||||||
|
```
|
||||||
|
|
||||||
|
To already initialize the `GameWidget` with a custom cursor, the `mouseCursor` property can be used
|
||||||
|
|
||||||
|
```dart
|
||||||
|
GameWidget(
|
||||||
|
game: MouseCursorGame(),
|
||||||
|
mouseCursor: SystemMouseCursors.move,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
## Event coordinate system
|
## Event coordinate system
|
||||||
|
|
||||||
On events that have positions, like for example `Tap*` or `Drag`, you will notice that the `eventPosition`
|
On events that have positions, like for example `Tap*` or `Drag`, you will notice that the `eventPosition`
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:dashbook/dashbook.dart';
|
import 'package:dashbook/dashbook.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../commons/commons.dart';
|
import '../../commons/commons.dart';
|
||||||
import 'draggables.dart';
|
import 'draggables.dart';
|
||||||
@ -7,6 +8,7 @@ import 'hoverables.dart';
|
|||||||
import 'joystick.dart';
|
import 'joystick.dart';
|
||||||
import 'joystick_advanced.dart';
|
import 'joystick_advanced.dart';
|
||||||
import 'keyboard.dart';
|
import 'keyboard.dart';
|
||||||
|
import 'mouse_cursor.dart';
|
||||||
import 'mouse_movement.dart';
|
import 'mouse_movement.dart';
|
||||||
import 'multitap.dart';
|
import 'multitap.dart';
|
||||||
import 'multitap_advanced.dart';
|
import 'multitap_advanced.dart';
|
||||||
@ -26,6 +28,18 @@ void addInputStories(Dashbook dashbook) {
|
|||||||
(_) => GameWidget(game: MouseMovementGame()),
|
(_) => GameWidget(game: MouseMovementGame()),
|
||||||
codeLink: baseLink('input/mouse_movement.dart'),
|
codeLink: baseLink('input/mouse_movement.dart'),
|
||||||
)
|
)
|
||||||
|
..add(
|
||||||
|
'Mouse Cursor',
|
||||||
|
(_) => GameWidget(
|
||||||
|
game: MouseCursorGame(),
|
||||||
|
mouseCursor: SystemMouseCursors.move,
|
||||||
|
),
|
||||||
|
codeLink: baseLink('input/mouse_cursor.dart'),
|
||||||
|
info: '''
|
||||||
|
Example showcasing the ability to change the game cursor in runtime
|
||||||
|
hover the little square to see the cursor changing
|
||||||
|
''',
|
||||||
|
)
|
||||||
..add(
|
..add(
|
||||||
'Scroll',
|
'Scroll',
|
||||||
(_) => GameWidget(game: ScrollGame()),
|
(_) => GameWidget(game: ScrollGame()),
|
||||||
|
|||||||
53
examples/lib/stories/input/mouse_cursor.dart
Normal file
53
examples/lib/stories/input/mouse_cursor.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flame/extensions.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:flame/palette.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class MouseCursorGame extends Game with MouseMovementDetector {
|
||||||
|
static const speed = 200;
|
||||||
|
static final Paint _blue = BasicPalette.blue.paint();
|
||||||
|
static final Paint _white = BasicPalette.white.paint();
|
||||||
|
static final Vector2 objSize = Vector2.all(150);
|
||||||
|
|
||||||
|
Vector2 position = Vector2(100, 100);
|
||||||
|
Vector2? target;
|
||||||
|
|
||||||
|
bool onTarget = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onMouseMove(PointerHoverInfo info) {
|
||||||
|
target = info.eventPosition.game;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect _toRect() => position.toPositionedRect(objSize);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {
|
||||||
|
canvas.drawRect(
|
||||||
|
_toRect(),
|
||||||
|
onTarget ? _blue : _white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
final target = this.target;
|
||||||
|
if (target != null) {
|
||||||
|
final hovering = _toRect().contains(target.toOffset());
|
||||||
|
if (hovering) {
|
||||||
|
if (!onTarget) {
|
||||||
|
//Entered
|
||||||
|
mouseCursor.value = SystemMouseCursors.grab;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onTarget) {
|
||||||
|
// Exited
|
||||||
|
mouseCursor.value = SystemMouseCursors.move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTarget = hovering;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
54.8
|
57.0
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
- Add `loadAllImages` to `Images`, which loads all images from the prefixed path
|
- Add `loadAllImages` to `Images`, which loads all images from the prefixed path
|
||||||
- Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`)
|
- Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`)
|
||||||
- Added `FocusNode` on the game widget and improved keyboard handling in the game.
|
- Added `FocusNode` on the game widget and improved keyboard handling in the game.
|
||||||
|
- Added ability to have custom mouse cursor on the `GameWidget` region
|
||||||
|
|
||||||
## [1.0.0-releasecandidate.13]
|
## [1.0.0-releasecandidate.13]
|
||||||
- Fix camera not ending up in the correct position on long jumps
|
- Fix camera not ending up in the correct position on long jumps
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -209,6 +210,11 @@ abstract class Game extends Projector {
|
|||||||
/// - GameWidget
|
/// - GameWidget
|
||||||
/// - [Game.overlays]
|
/// - [Game.overlays]
|
||||||
final overlays = ActiveOverlaysNotifier();
|
final overlays = ActiveOverlaysNotifier();
|
||||||
|
|
||||||
|
/// Used to change the mouse cursor of the GameWidget running this game.
|
||||||
|
/// Setting the value to null will make the GameWidget defer the choice
|
||||||
|
/// of the cursor to the closest region available on the tree.
|
||||||
|
final mouseCursor = ValueNotifier<MouseCursor?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance.
|
/// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance.
|
||||||
|
|||||||
@ -68,6 +68,10 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
|||||||
/// Defaults to true.
|
/// Defaults to true.
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// Initial mouse cursor for this [GameWidget]
|
||||||
|
/// mouse cursor can be changed in runtime using [Game.mouseCursor]
|
||||||
|
final MouseCursor? mouseCursor;
|
||||||
|
|
||||||
/// Renders a [game] in a flutter widget tree.
|
/// Renders a [game] in a flutter widget tree.
|
||||||
///
|
///
|
||||||
/// Ex:
|
/// Ex:
|
||||||
@ -116,6 +120,7 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
|||||||
this.initialActiveOverlays,
|
this.initialActiveOverlays,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.autofocus = true,
|
this.autofocus = true,
|
||||||
|
this.mouseCursor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Renders a [game] in a flutter widget tree alongside widgets overlays.
|
/// Renders a [game] in a flutter widget tree alongside widgets overlays.
|
||||||
@ -128,6 +133,8 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
|||||||
class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
||||||
Set<String> initialActiveOverlays = {};
|
Set<String> initialActiveOverlays = {};
|
||||||
|
|
||||||
|
MouseCursor? _mouseCursor;
|
||||||
|
|
||||||
Future<void>? _gameLoaderFuture;
|
Future<void>? _gameLoaderFuture;
|
||||||
|
|
||||||
Future<void> get _gameLoaderFutureCache =>
|
Future<void> get _gameLoaderFutureCache =>
|
||||||
@ -139,8 +146,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
|
|
||||||
// Add the initial overlays
|
// Add the initial overlays
|
||||||
_initActiveOverlays();
|
_initActiveOverlays();
|
||||||
|
addOverlaysListener();
|
||||||
|
|
||||||
addOverlaysListener(widget.game);
|
// Add the initial mouse cursor
|
||||||
|
_initMouseCursor();
|
||||||
|
addMouseCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initMouseCursor() {
|
||||||
|
if (widget.mouseCursor != null) {
|
||||||
|
widget.game.mouseCursor.value = widget.mouseCursor;
|
||||||
|
_mouseCursor = widget.game.mouseCursor.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initActiveOverlays() {
|
void _initActiveOverlays() {
|
||||||
@ -161,7 +178,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
|
|
||||||
// Reset the overlays
|
// Reset the overlays
|
||||||
_initActiveOverlays();
|
_initActiveOverlays();
|
||||||
addOverlaysListener(widget.game);
|
addOverlaysListener();
|
||||||
|
|
||||||
|
// Reset mouse cursor
|
||||||
|
_initMouseCursor();
|
||||||
|
addMouseCursorListener();
|
||||||
|
|
||||||
// Reset the loader future
|
// Reset the loader future
|
||||||
_gameLoaderFuture = null;
|
_gameLoaderFuture = null;
|
||||||
@ -174,8 +195,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
removeOverlaysListener(widget.game);
|
removeOverlaysListener(widget.game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addMouseCursorListener() {
|
||||||
|
widget.game.mouseCursor.addListener(onChangeMouseCursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChangeMouseCursor() {
|
||||||
|
setState(() {
|
||||||
|
_mouseCursor = widget.game.mouseCursor.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// widget overlay stuff
|
// widget overlay stuff
|
||||||
void addOverlaysListener(T game) {
|
void addOverlaysListener() {
|
||||||
widget.game.overlays.addListener(onChangeActiveOverlays);
|
widget.game.overlays.addListener(onChangeActiveOverlays);
|
||||||
initialActiveOverlays = widget.game.overlays.value;
|
initialActiveOverlays = widget.game.overlays.value;
|
||||||
}
|
}
|
||||||
@ -249,35 +280,38 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
// We can use Directionality.maybeOf when that method lands on stable
|
// We can use Directionality.maybeOf when that method lands on stable
|
||||||
final textDir = widget.textDirection ?? TextDirection.ltr;
|
final textDir = widget.textDirection ?? TextDirection.ltr;
|
||||||
|
|
||||||
return Focus(
|
return MouseRegion(
|
||||||
focusNode: widget.focusNode,
|
cursor: _mouseCursor ?? MouseCursor.defer,
|
||||||
autofocus: widget.autofocus,
|
child: Focus(
|
||||||
onKey: _handleKeyEvent,
|
focusNode: widget.focusNode,
|
||||||
child: Directionality(
|
autofocus: widget.autofocus,
|
||||||
textDirection: textDir,
|
onKey: _handleKeyEvent,
|
||||||
child: Container(
|
child: Directionality(
|
||||||
color: widget.game.backgroundColor(),
|
textDirection: textDir,
|
||||||
child: LayoutBuilder(
|
child: Container(
|
||||||
builder: (_, BoxConstraints constraints) {
|
color: widget.game.backgroundColor(),
|
||||||
widget.game.onResize(constraints.biggest.toVector2());
|
child: LayoutBuilder(
|
||||||
return FutureBuilder(
|
builder: (_, BoxConstraints constraints) {
|
||||||
future: _gameLoaderFutureCache,
|
widget.game.onResize(constraints.biggest.toVector2());
|
||||||
builder: (_, snapshot) {
|
return FutureBuilder(
|
||||||
if (snapshot.hasError) {
|
future: _gameLoaderFutureCache,
|
||||||
final errorBuilder = widget.errorBuilder;
|
builder: (_, snapshot) {
|
||||||
if (errorBuilder == null) {
|
if (snapshot.hasError) {
|
||||||
throw snapshot.error!;
|
final errorBuilder = widget.errorBuilder;
|
||||||
} else {
|
if (errorBuilder == null) {
|
||||||
return errorBuilder(context, snapshot.error!);
|
throw snapshot.error!;
|
||||||
|
} else {
|
||||||
|
return errorBuilder(context, snapshot.error!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
return Stack(children: stackedWidgets);
|
||||||
return Stack(children: stackedWidgets);
|
}
|
||||||
}
|
return widget.loadingBuilder?.call(context) ?? Container();
|
||||||
return widget.loadingBuilder?.call(context) ?? Container();
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
class TestGame extends Game {
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder byMouseCursor(MouseCursor cursor) {
|
||||||
|
return find.byWidgetPredicate(
|
||||||
|
(widget) => widget is MouseRegion && widget.cursor == cursor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameWidget - MouseCursor', () {
|
||||||
|
testWidgets('renders with the initial cursor', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: GameWidget(
|
||||||
|
game: TestGame(),
|
||||||
|
mouseCursor: SystemMouseCursors.grab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
byMouseCursor(SystemMouseCursors.grab),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('can change the cursor', (tester) async {
|
||||||
|
final game = TestGame();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: GameWidget(
|
||||||
|
game: game,
|
||||||
|
mouseCursor: SystemMouseCursors.grab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Making sure this cursor isn't showing yet
|
||||||
|
expect(byMouseCursor(SystemMouseCursors.copy), findsNothing);
|
||||||
|
|
||||||
|
game.mouseCursor.value = SystemMouseCursors.copy;
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
byMouseCursor(SystemMouseCursors.copy),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user