Skip to content

Commit

Permalink
Merge branch 'tom-anders-onTappedSquare'
Browse files Browse the repository at this point in the history
  • Loading branch information
veloce committed Jan 12, 2025
2 parents bc1bea6 + 47bec02 commit 3eeb08d
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 6.3.0

- Added an `onTouchedSquare` callback to `Chessboard`
- `Chessboard` now supports highlighting arbitrary squares.

## 6.2.3

- Fix board editor color filter
Expand Down
34 changes: 28 additions & 6 deletions lib/src/widgets/board.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class Chessboard extends StatefulWidget with ChessboardGeometry {
required this.fen,
this.opponentsPiecesUpsideDown = false,
this.lastMove,
this.squareHighlights = const IMapConst({}),
this.onTouchedSquare,
required this.game,
this.shapes,
this.annotations,
Expand All @@ -60,6 +62,8 @@ class Chessboard extends StatefulWidget with ChessboardGeometry {
required this.orientation,
required this.fen,
this.lastMove,
this.squareHighlights = const IMapConst({}),
this.onTouchedSquare,
this.shapes,
this.annotations,
}) : _size = size,
Expand All @@ -82,12 +86,21 @@ class Chessboard extends StatefulWidget with ChessboardGeometry {
/// If `true` the opponent`s pieces are displayed rotated by 180 degrees.
final bool opponentsPiecesUpsideDown;

/// Squares to highlight on the board.
final IMap<Square, SquareHighlight> squareHighlights;

/// FEN string describing the position of the board.
final String fen;

/// Last move played, used to highlight corresponding squares.
final Move? lastMove;

/// Callback called after a square has been touched.
///
/// This will be called even when the board is not interactable, with each [PointerDownEvent] that
/// targets a square.
final void Function(Square)? onTouchedSquare;

/// Game state of the board.
///
/// If `null`, the board cannot be interacted with.
Expand Down Expand Up @@ -267,6 +280,15 @@ class _BoardState extends State<Chessboard> {
square: checkSquare,
child: CheckHighlight(size: widget.squareSize),
),
for (final MapEntry(key: square, value: highlight)
in widget.squareHighlights.entries)
PositionedSquare(
key: ValueKey('${square.name}-highlight'),
size: widget.size,
orientation: widget.orientation,
square: square,
child: highlight,
),
];

final List<Widget> objects = [
Expand Down Expand Up @@ -352,13 +374,11 @@ class _BoardState extends State<Chessboard> {
),
];

final enableListeners = widget.interactive || settings.drawShape.enable;

final board = Listener(
onPointerDown: enableListeners ? _onPointerDown : null,
onPointerMove: enableListeners ? _onPointerMove : null,
onPointerUp: enableListeners ? _onPointerUp : null,
onPointerCancel: enableListeners ? _onPointerCancel : null,
onPointerDown: _onPointerDown,
onPointerMove: _onPointerMove,
onPointerUp: _onPointerUp,
onPointerCancel: _onPointerCancel,
child: SizedBox.square(
key: const ValueKey('board-container'),
dimension: widget.size,
Expand Down Expand Up @@ -549,6 +569,8 @@ class _BoardState extends State<Chessboard> {
final square = widget.offsetSquare(details.localPosition);
if (square == null) return;

widget.onTouchedSquare?.call(square);

final Piece? piece = pieces[square];

if (widget.settings.drawShape.enable) {
Expand Down
106 changes: 104 additions & 2 deletions test/widgets/board_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,30 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dartchess/dartchess.dart';
import 'package:chessground/chessground.dart';
import 'package:mocktail/mocktail.dart';

const boardSize = 200.0;
const squareSize = boardSize / 8;

class OnTappedSquareMock extends Mock {
void call(Square square);
}

void main() {
group('Non-interactive board', () {
const viewOnlyBoard = Chessboard.fixed(
final onTouchedSquare = OnTappedSquareMock();
tearDown(() {
reset(onTouchedSquare);
});

final viewOnlyBoard = Chessboard.fixed(
size: boardSize,
orientation: Side.white,
fen: kInitialFEN,
settings: ChessboardSettings(
settings: const ChessboardSettings(
drawShape: DrawShapeOptions(enable: true),
),
onTouchedSquare: onTouchedSquare.call,
);

testWidgets('initial position display', (WidgetTester tester) async {
Expand All @@ -34,6 +45,9 @@ void main() {
await tester.pump();

expect(find.byKey(const Key('e2-selected')), findsNothing);

verify(() => onTouchedSquare.call(Square.e2)).called(1);
verifyNoMoreInteractions(onTouchedSquare);
});

testWidgets('background is constrained to the size of the board', (
Expand Down Expand Up @@ -113,11 +127,13 @@ void main() {
border: BoardBorder(width: 16.0, color: Color(0xFF000000)),
),
]) {
final onTouchedSquare = OnTappedSquareMock();
await tester.pumpWidget(
_TestApp(
initialPlayerSide: PlayerSide.both,
settings: settings,
key: ValueKey(settings.hashCode),
onTouchedSquare: onTouchedSquare.call,
),
);
await tester.tap(find.byKey(const Key('a2-whitepawn')));
Expand Down Expand Up @@ -156,6 +172,17 @@ void main() {
await tester.tap(find.byKey(const Key('e7-blackpawn')));
await tester.pump();
expect(find.byKey(const Key('e7-selected')), findsNothing);

verifyInOrder([
() => onTouchedSquare.call(Square.a2),
() => onTouchedSquare.call(Square.a2),
() => onTouchedSquare.call(Square.a1),
() => onTouchedSquare.call(Square.e7),
() => onTouchedSquare.call(Square.a1),
() => onTouchedSquare.call(Square.c4),
() => onTouchedSquare.call(Square.e7),
]);
verifyNoMoreInteractions(onTouchedSquare);
}
});

Expand Down Expand Up @@ -205,11 +232,13 @@ void main() {
border: BoardBorder(width: 16.0, color: Color(0xFF000000)),
),
]) {
final onTouchedSquare = OnTappedSquareMock();
await tester.pumpWidget(
_TestApp(
initialPlayerSide: PlayerSide.both,
settings: settings,
key: ValueKey(settings.hashCode),
onTouchedSquare: onTouchedSquare.call,
),
);
await tester.tap(find.byKey(const Key('e2-whitepawn')));
Expand All @@ -229,6 +258,12 @@ void main() {
expect(find.byKey(const Key('e2-whitepawn')), findsNothing);
expect(find.byKey(const Key('e2-lastMove')), findsOneWidget);
expect(find.byKey(const Key('e4-lastMove')), findsOneWidget);

verifyInOrder([
() => onTouchedSquare.call(Square.e2),
() => onTouchedSquare.call(Square.e2),
]);
verifyNoMoreInteractions(onTouchedSquare);
}
});

Expand Down Expand Up @@ -598,6 +633,69 @@ void main() {
});
});

testWidgets('onTouchedSquare callback', (WidgetTester tester) async {
final controller = StreamController<GameEvent>.broadcast();

addTearDown(() {
controller.close();
});

final onTouchedSquare = OnTappedSquareMock();
await tester.pumpWidget(
_TestApp(
initialPlayerSide: PlayerSide.white,
gameEventStream: controller.stream,
onTouchedSquare: onTouchedSquare.call,
),
);

// Trigger callback by tapping a square with a piece on it
await tester.tapAt(squareOffset(tester, Square.a1));

// Trigger callback by tapping an empty square
await tester.tapAt(squareOffset(tester, Square.e4));

// Drag a piece to the same square -> should trigger callback
await tester.dragFrom(
squareOffset(tester, Square.a2),
const Offset(0, -(squareSize / 2)),
);

// Drag from a empty square to the same square -> should trigger callback
await tester.dragFrom(
squareOffset(tester, Square.a4),
const Offset(0, -(squareSize / 2)),
);

// Drag from an empty square another empty square -> should trigger callback on 1st square
await tester.dragFrom(
squareOffset(tester, Square.a4),
const Offset(0, -squareSize),
);

// Drag piece to a different square (i.e. make a move) -> should trigger callback on 1st square
await tester.dragFrom(
squareOffset(tester, Square.a2),
const Offset(0, -squareSize),
);

// Callback should be triggered even if the board is non-interactive
controller.add(GameEvent.nonInteractiveBoardEvent);
await tester.pump(const Duration(milliseconds: 1));
await tester.tapAt(squareOffset(tester, Square.e3));

verifyInOrder([
() => onTouchedSquare(Square.a1),
() => onTouchedSquare(Square.e4),
() => onTouchedSquare(Square.a2),
() => onTouchedSquare(Square.a4),
() => onTouchedSquare(Square.a4),
() => onTouchedSquare(Square.a2),
() => onTouchedSquare(Square.e3),
]);
verifyNoMoreInteractions(onTouchedSquare);
});

group('Promotion', () {
testWidgets('can display the selector', (WidgetTester tester) async {
await tester.pumpWidget(
Expand Down Expand Up @@ -1555,6 +1653,7 @@ class _TestApp extends StatefulWidget {
this.enableDrawingShapes = false,
this.shouldPlayOpponentMove = false,
this.gameEventStream,
this.onTouchedSquare,
super.key,
});

Expand All @@ -1573,6 +1672,8 @@ class _TestApp extends StatefulWidget {
/// A stream of game events
final Stream<GameEvent>? gameEventStream;

final void Function(Square)? onTouchedSquare;

@override
State<_TestApp> createState() => _TestAppState();
}
Expand Down Expand Up @@ -1729,6 +1830,7 @@ class _TestAppState extends State<_TestApp> {
},
),
),
onTouchedSquare: widget.onTouchedSquare,
shapes: shapes,
),
),
Expand Down

0 comments on commit 3eeb08d

Please sign in to comment.