mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 00:17:20 +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/
|
||||
|
||||
coverage
|
||||
|
||||
.fvm
|
||||
|
||||
@ -102,6 +102,22 @@ and [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-cla
|
||||
also read more about Flutter's gestures
|
||||
[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
|
||||
|
||||
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:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../commons/commons.dart';
|
||||
import 'draggables.dart';
|
||||
@ -7,6 +8,7 @@ import 'hoverables.dart';
|
||||
import 'joystick.dart';
|
||||
import 'joystick_advanced.dart';
|
||||
import 'keyboard.dart';
|
||||
import 'mouse_cursor.dart';
|
||||
import 'mouse_movement.dart';
|
||||
import 'multitap.dart';
|
||||
import 'multitap_advanced.dart';
|
||||
@ -26,6 +28,18 @@ void addInputStories(Dashbook dashbook) {
|
||||
(_) => GameWidget(game: MouseMovementGame()),
|
||||
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(
|
||||
'Scroll',
|
||||
(_) => 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
|
||||
- Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`)
|
||||
- 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]
|
||||
- Fix camera not ending up in the correct position on long jumps
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -209,6 +210,11 @@ abstract class Game extends Projector {
|
||||
/// - GameWidget
|
||||
/// - [Game.overlays]
|
||||
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.
|
||||
|
||||
@ -68,6 +68,10 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
||||
/// Defaults to true.
|
||||
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.
|
||||
///
|
||||
/// Ex:
|
||||
@ -116,6 +120,7 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
||||
this.initialActiveOverlays,
|
||||
this.focusNode,
|
||||
this.autofocus = true,
|
||||
this.mouseCursor,
|
||||
}) : super(key: key);
|
||||
|
||||
/// 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>> {
|
||||
Set<String> initialActiveOverlays = {};
|
||||
|
||||
MouseCursor? _mouseCursor;
|
||||
|
||||
Future<void>? _gameLoaderFuture;
|
||||
|
||||
Future<void> get _gameLoaderFutureCache =>
|
||||
@ -139,8 +146,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
||||
|
||||
// Add the initial overlays
|
||||
_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() {
|
||||
@ -161,7 +178,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
||||
|
||||
// Reset the overlays
|
||||
_initActiveOverlays();
|
||||
addOverlaysListener(widget.game);
|
||||
addOverlaysListener();
|
||||
|
||||
// Reset mouse cursor
|
||||
_initMouseCursor();
|
||||
addMouseCursorListener();
|
||||
|
||||
// Reset the loader future
|
||||
_gameLoaderFuture = null;
|
||||
@ -174,8 +195,18 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
||||
removeOverlaysListener(widget.game);
|
||||
}
|
||||
|
||||
void addMouseCursorListener() {
|
||||
widget.game.mouseCursor.addListener(onChangeMouseCursor);
|
||||
}
|
||||
|
||||
void onChangeMouseCursor() {
|
||||
setState(() {
|
||||
_mouseCursor = widget.game.mouseCursor.value;
|
||||
});
|
||||
}
|
||||
|
||||
// widget overlay stuff
|
||||
void addOverlaysListener(T game) {
|
||||
void addOverlaysListener() {
|
||||
widget.game.overlays.addListener(onChangeActiveOverlays);
|
||||
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
|
||||
final textDir = widget.textDirection ?? TextDirection.ltr;
|
||||
|
||||
return Focus(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onKey: _handleKeyEvent,
|
||||
child: Directionality(
|
||||
textDirection: textDir,
|
||||
child: Container(
|
||||
color: widget.game.backgroundColor(),
|
||||
child: LayoutBuilder(
|
||||
builder: (_, BoxConstraints constraints) {
|
||||
widget.game.onResize(constraints.biggest.toVector2());
|
||||
return FutureBuilder(
|
||||
future: _gameLoaderFutureCache,
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
final errorBuilder = widget.errorBuilder;
|
||||
if (errorBuilder == null) {
|
||||
throw snapshot.error!;
|
||||
} else {
|
||||
return errorBuilder(context, snapshot.error!);
|
||||
return MouseRegion(
|
||||
cursor: _mouseCursor ?? MouseCursor.defer,
|
||||
child: Focus(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onKey: _handleKeyEvent,
|
||||
child: Directionality(
|
||||
textDirection: textDir,
|
||||
child: Container(
|
||||
color: widget.game.backgroundColor(),
|
||||
child: LayoutBuilder(
|
||||
builder: (_, BoxConstraints constraints) {
|
||||
widget.game.onResize(constraints.biggest.toVector2());
|
||||
return FutureBuilder(
|
||||
future: _gameLoaderFutureCache,
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
final errorBuilder = widget.errorBuilder;
|
||||
if (errorBuilder == null) {
|
||||
throw snapshot.error!;
|
||||
} else {
|
||||
return errorBuilder(context, snapshot.error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Stack(children: stackedWidgets);
|
||||
}
|
||||
return widget.loadingBuilder?.call(context) ?? Container();
|
||||
},
|
||||
);
|
||||
},
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Stack(children: stackedWidgets);
|
||||
}
|
||||
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