feat: Add advanced button component (#2742)

New button with support for multiple states:
<img width="278" alt="image"
src="https://github.com/flame-engine/flame/assets/18004353/041c1105-8991-4976-b1a2-0553c149ec4e">
This commit is contained in:
Denis Gladky
2023-10-10 22:43:28 +03:00
committed by GitHub
parent cccae2e147
commit 97fff0ed2b
9 changed files with 1098 additions and 0 deletions

View File

@ -142,3 +142,33 @@ else which isn't a pure sprite.
Flame has a separate plugin to support external game controllers (gamepads), check
[here](https://github.com/flame-engine/flame_gamepad) for more information.
## AdvancedButtonComponent
The `AdvancedButtonComponent` have separate states for each of the different pointer phases.
The skin can be customized for each state and each skin is represented by a `PositionComponent`.
These are the fields that can be used to customize the looks of the `AdvancedButtonComponent`:
- `defaultSkin`: Component that will be displayed by default on the button.
- `downSkin`: Component displayed when the button is clicked or tapped.
- `hoverSkin`: Component displayed when the button is hovered. (desktop and web).
- `defaultLabel`: Component shown on top of skins. Automatically aligned to center.
- `disabledSkin`: Component displayed when button is disabled.
- `disabledLabel`: Component shown on top of skins when button is disabled.
## ToggleButtonComponent
The [ToggleButtonComponent] is an [AdvancedButtonComponent] that can switch between selected
and not selected.
In addition to the already existing skins, the [ToggleButtonComponent] contains the following skins:
- `defaultSelectedSkin`: The component to display when the button is selected.
- `downAndSelectedSkin`: The component that is displayed when the selectable button is selected and
pressed.
- `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.

View File

@ -0,0 +1,125 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:flutter/painting.dart';
class AdvancedButtonExample extends FlameGame {
static const String description =
'''This example shows how you can use a button with different states''';
@override
Future<void> onLoad() async {
final defaultButton = DefaultButton();
defaultButton.position = Vector2(50, 50);
defaultButton.size = Vector2(250, 50);
add(defaultButton);
final disableButton = DisableButton();
disableButton.isDisabled = true;
disableButton.position = Vector2(400, 50);
disableButton.size = defaultButton.size;
add(disableButton);
final toggleButton = ToggleButton();
toggleButton.position = Vector2(50, 150);
toggleButton.size = defaultButton.size;
add(toggleButton);
}
}
class ToggleButton extends ToggleButtonComponent {
@override
Future<void> onLoad() async {
super.onLoad();
defaultLabel = TextComponent(
text: 'Toggle button',
textRenderer: TextPaint(
style: TextStyle(
fontSize: 24,
color: BasicPalette.white.color,
),
),
);
defaultSelectedLabel = TextComponent(
text: 'Toggle button',
textRenderer: TextPaint(
style: TextStyle(
fontSize: 24,
color: BasicPalette.red.color,
),
),
);
defaultSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 200, 0, 1));
hoverSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 180, 0, 1));
downSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 100, 0, 1));
defaultSelectedSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 0, 200, 1));
hoverAndSelectedSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 0, 180, 1));
downAndSelectedSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 0, 100, 1));
}
}
class DefaultButton extends AdvancedButtonComponent {
@override
Future<void> onLoad() async {
super.onLoad();
defaultLabel = TextComponent(text: 'Default button');
defaultSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 200, 0, 1));
hoverSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 180, 0, 1));
downSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 100, 0, 1));
}
}
class DisableButton extends AdvancedButtonComponent {
@override
Future<void> onLoad() async {
super.onLoad();
disabledLabel = TextComponent(text: 'Disabled button');
defaultSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(0, 255, 0, 1));
disabledSkin = RoundedRectComponent()
..setColor(const Color.fromRGBO(100, 100, 100, 1));
}
}
class RoundedRectComponent extends PositionComponent with HasPaint {
@override
void render(Canvas canvas) {
canvas.drawRRect(
RRect.fromLTRBAndCorners(
0,
0,
width,
height,
topLeft: Radius.circular(height),
topRight: Radius.circular(height),
bottomRight: Radius.circular(height),
bottomLeft: Radius.circular(height),
),
paint,
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:dashbook/dashbook.dart';
import 'package:examples/commons/commons.dart';
import 'package:examples/stories/input/advanced_button_example.dart';
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';
@ -129,5 +130,11 @@ void addInputStories(Dashbook dashbook) {
(_) => GameWidget(game: JoystickAdvancedExample()),
codeLink: baseLink('input/joystick_advanced_example.dart'),
info: JoystickAdvancedExample.description,
)
..add(
'Advanced Button',
(_) => GameWidget(game: AdvancedButtonExample()),
codeLink: baseLink('input/advanced_button_example.dart'),
info: AdvancedButtonExample.description,
);
}

View File

@ -13,8 +13,10 @@ export 'src/components/core/position_type.dart';
export 'src/components/custom_painter_component.dart';
export 'src/components/fps_component.dart';
export 'src/components/fps_text_component.dart';
export 'src/components/input/advanced_button_component.dart';
export 'src/components/input/joystick_component.dart';
export 'src/components/input/keyboard_listener_component.dart';
export 'src/components/input/toggle_button_component.dart';
export 'src/components/isometric_tile_map_component.dart';
export 'src/components/mixins/component_viewport_margin.dart';
export 'src/components/mixins/coordinate_transform.dart';

View File

@ -0,0 +1,292 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/layout.dart';
import 'package:flutter/foundation.dart';
/// The [AdvancedButtonComponent] has different skins for
/// different button states.
/// The [defaultSkin] must be added to the constructor or
/// if you are inheriting - defined in the onLod method.
///
/// The label is a [PositionComponent] and is added
/// to the foreground of the button. The label is automatically aligned to
/// the center of the button.
///
/// Note: You have to set the skins that you want to use ([defaultSkin],
/// [downSkin], [hoverSkin], [disabledSkin], [defaultLabel]) in [onLoad]
/// if you are not passing them in through the constructor.
class AdvancedButtonComponent extends PositionComponent
with HoverCallbacks, TapCallbacks {
AdvancedButtonComponent({
this.onPressed,
this.onChangeState,
PositionComponent? defaultSkin,
PositionComponent? downSkin,
PositionComponent? hoverSkin,
PositionComponent? disabledSkin,
PositionComponent? defaultLabel,
PositionComponent? disabledLabel,
super.size,
super.position,
super.scale,
super.angle,
super.anchor,
super.children,
super.priority,
}) {
this.defaultSkin = defaultSkin;
this.downSkin = downSkin;
this.hoverSkin = hoverSkin;
this.disabledSkin = disabledSkin;
this.defaultLabel = defaultLabel;
this.disabledLabel = disabledLabel;
size.addListener(_updateSizes);
}
/// Callback for what should happen when the button is pressed.
void Function()? onPressed;
/// Callback when button state changes
void Function(ButtonState state)? onChangeState;
@mustCallSuper
@override
Future<void> onLoad() async {
super.onLoad();
add(skinContainer);
add(labelAlignContainer);
}
@protected
final skinContainer = Component();
@protected
AlignComponent labelAlignContainer = AlignComponent(alignment: Anchor.center);
@override
@mustCallSuper
void onMount() {
super.onMount();
assert(
defaultSkin != null,
'The defaultSkin has to either be passed '
'in as an argument or set in onLoad',
);
if (_state.isDefault && !contains(defaultSkin!)) {
defaultSkin!.parent = skinContainer;
}
}
@protected
bool isPressed = false;
@override
@mustCallSuper
void onTapDown(TapDownEvent event) {
if (_isDisabled) {
return;
}
onPressed?.call();
isPressed = true;
updateState();
}
@override
void onTapUp(TapUpEvent event) {
isPressed = false;
updateState();
}
@override
void onHoverEnter() {
updateState();
}
@override
void onHoverExit() {
isPressed = false;
updateState();
}
Map<ButtonState, PositionComponent?> skinsMap = {};
PositionComponent? get defaultSkin => skinsMap[ButtonState.up];
set defaultSkin(PositionComponent? value) {
skinsMap[ButtonState.up] = value;
if (size.isZero()) {
size = skinsMap[ButtonState.up]?.size ?? Vector2.zero();
}
invalidateSkins();
}
set downSkin(PositionComponent? value) {
skinsMap[ButtonState.down] = value;
invalidateSkins();
}
set hoverSkin(PositionComponent? value) {
skinsMap[ButtonState.hover] = value;
invalidateSkins();
}
set disabledSkin(PositionComponent? value) {
skinsMap[ButtonState.disabled] = value;
invalidateSkins();
}
Map<ButtonState, PositionComponent?> labelsMap = {};
PositionComponent? get defaultLabel => labelsMap[ButtonState.up];
set defaultLabel(PositionComponent? value) {
labelsMap[ButtonState.up] = value;
updateLabel();
}
set disabledLabel(PositionComponent? value) {
labelsMap[ButtonState.disabled] = value;
updateLabel();
}
@protected
void invalidateSkins() {
_updateSizes();
_updateSkin();
}
bool _isDisabled = false;
bool get isDisabled => _isDisabled;
set isDisabled(bool value) {
if (_isDisabled == value) {
return;
}
_isDisabled = value;
updateState();
}
void _updateSizes() {
for (final skin in skinsMap.values) {
skin?.size = size;
}
}
@protected
void updateState() {
if (isDisabled) {
setState(ButtonState.disabled);
return;
}
if (isPressed) {
setState(ButtonState.down);
return;
}
if (isHovered) {
setState(ButtonState.hover);
return;
}
setState(ButtonState.up);
}
ButtonState _state = ButtonState.up;
@protected
void setState(ButtonState value) {
if (_state == value) {
return;
}
_state = value;
_updateSkin();
updateLabel();
onChangeState?.call(_state);
}
void _updateSkin() {
_removeSkins();
setSkin(_state);
}
@protected
void setSkin(ButtonState state) {
(skinsMap[state] ?? defaultSkin)?.parent = skinContainer;
}
void _removeSkins() {
for (final skins in skinsMap.values) {
skins?.parent = null;
}
}
@protected
void updateLabel() {
_removeLabels();
addLabel(_state);
}
@protected
void addLabel(ButtonState state) {
labelAlignContainer.child = labelsMap[state] ?? defaultLabel;
}
void _removeLabels() {
for (final label in labelsMap.values) {
label?.parent = null;
}
}
@protected
bool hasSkinForState(ButtonState state) {
return skinsMap[state] != null;
}
}
enum ButtonState {
up,
upAndSelected,
down,
downAndSelected,
hover,
hoverAndSelected,
disabled,
disabledAndSelected;
const ButtonState();
bool get isDefault {
return this == ButtonState.up;
}
bool get isDefaultSelected {
return this == ButtonState.upAndSelected;
}
bool get isNotDefault {
return !isDefault;
}
bool get isDown {
return this == ButtonState.down;
}
bool get isDownAndSelected {
return this == ButtonState.downAndSelected;
}
bool get isHover {
return this == ButtonState.hover;
}
bool get isHoverAndSelected {
return this == ButtonState.hoverAndSelected;
}
bool get isDisabled {
return this == ButtonState.disabled;
}
bool get isDisabledAndSelected {
return this == ButtonState.disabledAndSelected;
}
}

View File

@ -0,0 +1,167 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flutter/foundation.dart';
/// The [ToggleButtonComponent] is an [AdvancedButtonComponent] that can switch
/// between the selected and not selected state, imagine for example a switch
/// widget or a tab that can be selected.
///
/// Note: You have to set the [defaultSkin], [defaultSelectedSkin]
/// and other skins that you want to use in [onLoad] if you are not passed in
/// through the constructor.
class ToggleButtonComponent extends AdvancedButtonComponent {
ToggleButtonComponent({
super.onPressed,
this.onSelectedChanged,
super.onChangeState,
super.defaultSkin,
super.downSkin,
super.hoverSkin,
super.disabledSkin,
PositionComponent? defaultSelectedSkin,
PositionComponent? downAndSelectedSkin,
PositionComponent? hoverAndSelectedSkin,
PositionComponent? disabledAndSelectedSkin,
super.defaultLabel,
super.disabledLabel,
PositionComponent? defaultSelectedLabel,
PositionComponent? disabledAndSelectedLabel,
super.size,
super.position,
super.scale,
super.angle,
super.anchor,
super.children,
super.priority,
}) {
this.defaultSelectedSkin = defaultSelectedSkin;
this.downAndSelectedSkin = downAndSelectedSkin;
this.hoverAndSelectedSkin = hoverAndSelectedSkin;
this.disabledAndSelectedSkin = disabledAndSelectedSkin;
this.defaultSelectedLabel = defaultSelectedLabel;
this.disabledAndSelectedLabel = disabledAndSelectedLabel;
}
/// Callback when button selected changed
ValueChanged<bool>? onSelectedChanged;
@override
@mustCallSuper
void onMount() {
assert(
defaultSelectedSkin != null,
'The defaultSelectedSkin has to either be passed '
'in as an argument or set in onLoad',
);
super.onMount();
}
PositionComponent? get defaultSelectedSkin =>
skinsMap[ButtonState.upAndSelected];
set defaultSelectedSkin(PositionComponent? value) {
skinsMap[ButtonState.upAndSelected] = value;
invalidateSkins();
}
set downAndSelectedSkin(PositionComponent? value) {
skinsMap[ButtonState.downAndSelected] = value;
invalidateSkins();
}
set hoverAndSelectedSkin(PositionComponent? value) {
skinsMap[ButtonState.hoverAndSelected] = value;
invalidateSkins();
}
set disabledAndSelectedSkin(PositionComponent? value) {
skinsMap[ButtonState.disabledAndSelected] = value;
invalidateSkins();
}
PositionComponent? get defaultSelectedLabel =>
labelsMap[ButtonState.upAndSelected];
set defaultSelectedLabel(PositionComponent? value) {
labelsMap[ButtonState.upAndSelected] = value;
updateLabel();
}
set disabledAndSelectedLabel(PositionComponent? value) {
labelsMap[ButtonState.disabledAndSelected] = value;
updateLabel();
}
@override
void onTapUp(TapUpEvent event) {
isSelected = !_isSelected;
super.onTapUp(event);
}
bool _isSelected = false;
bool get isSelected => _isSelected;
set isSelected(bool value) {
if (_isSelected == value) {
return;
}
_isSelected = value;
updateState();
onSelectedChanged?.call(_isSelected);
}
@override
@protected
void setSkin(ButtonState state) {
var skin = skinsMap[state];
if (state.isDisabledAndSelected && !hasSkinForState(state)) {
skin = skinsMap[ButtonState.disabled];
}
if (state.isDownAndSelected && !hasSkinForState(state)) {
skin = skinsMap[ButtonState.down];
}
if (state.isHoverAndSelected && !hasSkinForState(state)) {
skin = skinsMap[ButtonState.hover];
}
if (state.isDownAndSelected && !hasSkinForState(state)) {
skin = skinsMap[ButtonState.down];
}
skin = skin ?? (isSelected ? defaultSelectedSkin : defaultSkin);
skin?.parent = skinContainer;
}
@override
@protected
void addLabel(ButtonState state) {
labelAlignContainer.child =
labelsMap[state] ?? (isSelected ? defaultSelectedLabel : defaultLabel);
}
@mustCallSuper
@protected
@override
void updateState() {
if (isDisabled) {
setState(
_isSelected ? ButtonState.disabledAndSelected : ButtonState.disabled,
);
return;
}
if (isPressed) {
setState(
_isSelected ? ButtonState.downAndSelected : ButtonState.down,
);
return;
}
if (isHovered) {
setState(
_isSelected ? ButtonState.hoverAndSelected : ButtonState.hover,
);
return;
}
setState(
_isSelected ? ButtonState.upAndSelected : ButtonState.up,
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

View File

@ -0,0 +1,221 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AdvancedButtonComponent', () {
testGolden(
'label renders correctly',
(game) async {
await game.add(
AdvancedButtonComponent(
defaultSkin: RectangleComponent(size: Vector2(40, 20)),
defaultLabel: RectangleComponent(
size: Vector2(10, 5),
paint: Paint()..color = const Color(0xFFFF0000),
),
),
);
},
size: Vector2(50, 30),
goldenFile: '../_goldens/advanced_button_component.png',
);
testWithFlameGame('correctly registers taps', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(200);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final AdvancedButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = AdvancedButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
expect(pressedTimes, 0);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(1, TapDownDetails());
expect(pressedTimes, 0);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(
globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),
),
);
expect(pressedTimes, 0);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: buttonPosition.toOffset()),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: buttonPosition.toOffset()),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: buttonPosition.toOffset()),
);
tapDispatcher.handleTapCancel(1);
expect(pressedTimes, 2);
});
testWithFlameGame('correctly registers taps onGameResize', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final AdvancedButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = AdvancedButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
final previousPosition =
button.positionOfAnchor(Anchor.center).toOffset();
game.onGameResize(initialGameSize * 2);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
tapDispatcher.handleTapCancel(1);
expect(pressedTimes, 2);
});
testWithFlameGame('correctly work isDisabled', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final AdvancedButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = AdvancedButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
button.isDisabled = true;
final previousPosition =
button.positionOfAnchor(Anchor.center).toOffset();
game.onGameResize(initialGameSize * 2);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 0);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 0);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
tapDispatcher.handleTapCancel(1);
expect(pressedTimes, 0);
});
testWidgets(
'[#1723] can be pressed while the engine is paused',
(tester) async {
final game = FlameGame();
game.add(
AdvancedButtonComponent(
defaultSkin: CircleComponent(radius: 40),
downSkin: CircleComponent(radius: 40),
position: Vector2(400, 300),
anchor: Anchor.center,
onPressed: () {
game.pauseEngine();
game.overlays.add('pause-menu');
},
),
);
await tester.pumpWidget(
GameWidget(
game: game,
overlayBuilderMap: {
'pause-menu': (context, _) {
return SimpleStatelessWidget(
build: (context) {
return Center(
child: OutlinedButton(
onPressed: () {
game.overlays.remove('pause-menu');
game.resumeEngine();
},
child: const Text('Resume'),
),
);
},
);
},
},
),
);
await tester.pump();
await tester.pump();
await tester.tapAt(const Offset(400, 300));
await tester.pump(const Duration(seconds: 1));
expect(game.paused, true);
await tester.tapAt(const Offset(400, 300));
await tester.pump(const Duration(seconds: 1));
expect(game.paused, false);
},
);
});
}
class SimpleStatelessWidget extends StatelessWidget {
const SimpleStatelessWidget({
required Widget Function(BuildContext) build,
super.key,
}) : _build = build;
final Widget Function(BuildContext) _build;
@override
Widget build(BuildContext context) => _build(context);
}

View File

@ -0,0 +1,254 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ToggleButtonComponent', () {
testWithFlameGame('correctly registers taps', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(200);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final ToggleButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = ToggleButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
defaultSelectedSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
expect(pressedTimes, 0);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(1, TapDownDetails());
expect(pressedTimes, 0);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(
globalPosition: button.positionOfAnchor(Anchor.center).toOffset(),
),
);
expect(pressedTimes, 0);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: buttonPosition.toOffset()),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: buttonPosition.toOffset()),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: buttonPosition.toOffset()),
);
tapDispatcher.handleTapCancel(1);
expect(pressedTimes, 2);
});
testWithFlameGame('correctly registers taps onGameResize', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final ToggleButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = ToggleButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
defaultSelectedSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
final previousPosition =
button.positionOfAnchor(Anchor.center).toOffset();
game.onGameResize(initialGameSize * 2);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 1);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
tapDispatcher.handleTapCancel(1);
expect(pressedTimes, 2);
});
testWithFlameGame('correctly work isDisabled', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final ToggleButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = ToggleButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
defaultSelectedSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
button.isDisabled = true;
final previousPosition =
button.positionOfAnchor(Anchor.center).toOffset();
game.onGameResize(initialGameSize * 2);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 0);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: previousPosition),
);
expect(pressedTimes, 0);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
tapDispatcher.handleTapCancel(1);
expect(pressedTimes, 0);
});
testWithFlameGame('toggle works correctly', (game) async {
var pressedTimes = 0;
final initialGameSize = Vector2.all(100);
final componentSize = Vector2.all(10);
final buttonPosition = Vector2.all(100);
late final ToggleButtonComponent button;
game.onGameResize(initialGameSize);
await game.ensureAdd(
button = ToggleButtonComponent(
defaultSkin: RectangleComponent(size: componentSize),
defaultSelectedSkin: RectangleComponent(size: componentSize),
onPressed: () => pressedTimes++,
position: buttonPosition,
size: componentSize,
),
);
final previousPosition =
button.positionOfAnchor(Anchor.center).toOffset();
game.onGameResize(initialGameSize * 2);
final tapDispatcher = game.firstChild<MultiTapDispatcher>()!;
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
expect(button.isSelected, false);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: previousPosition),
);
expect(button.isSelected, true);
tapDispatcher.handleTapDown(
1,
TapDownDetails(globalPosition: previousPosition),
);
expect(button.isSelected, true);
tapDispatcher.handleTapUp(
1,
createTapUpDetails(globalPosition: previousPosition),
);
expect(button.isSelected, false);
});
testWidgets(
'[#1723] can be pressed while the engine is paused',
(tester) async {
final game = FlameGame();
game.add(
ToggleButtonComponent(
defaultSkin: CircleComponent(radius: 40),
downSkin: CircleComponent(radius: 40),
defaultSelectedSkin: CircleComponent(radius: 40),
position: Vector2(400, 300),
anchor: Anchor.center,
onPressed: () {
game.pauseEngine();
game.overlays.add('pause-menu');
},
),
);
await tester.pumpWidget(
GameWidget(
game: game,
overlayBuilderMap: {
'pause-menu': (context, _) {
return SimpleStatelessWidget(
build: (context) {
return Center(
child: OutlinedButton(
onPressed: () {
game.overlays.remove('pause-menu');
game.resumeEngine();
},
child: const Text('Resume'),
),
);
},
);
},
},
),
);
await tester.pump();
await tester.pump();
await tester.tapAt(const Offset(400, 300));
await tester.pump(const Duration(seconds: 1));
expect(game.paused, true);
await tester.tapAt(const Offset(400, 300));
await tester.pump(const Duration(seconds: 1));
expect(game.paused, false);
},
);
});
}
class SimpleStatelessWidget extends StatelessWidget {
const SimpleStatelessWidget({
required Widget Function(BuildContext) build,
super.key,
}) : _build = build;
final Widget Function(BuildContext) _build;
@override
Widget build(BuildContext context) => _build(context);
}