diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ff8e2..e364dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.2.0 + +- Add `pieceShiftMethod` to `BoardSetttings`, with possible values: `either` (default), `drag`, or `tapTwoSquares`. + ## 3.1.2 - Any simultaneous touch on the board will now cancel the current piece diff --git a/example/lib/main.dart b/example/lib/main.dart index 84a6cff..438d1f4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,6 +28,17 @@ class MyApp extends StatelessWidget { } } +String pieceShiftMethodLabel(PieceShiftMethod method) { + switch (method) { + case PieceShiftMethod.drag: + return 'Drag'; + case PieceShiftMethod.tapTwoSquares: + return 'Tap two squares'; + case PieceShiftMethod.either: + return 'Either'; + } +} + enum Mode { botPlay, freePlay, @@ -51,6 +62,7 @@ class _HomePageState extends State { ValidMoves validMoves = IMap(const {}); Side sideToMove = Side.white; PieceSet pieceSet = PieceSet.merida; + PieceShiftMethod pieceShiftMethod = PieceShiftMethod.either; BoardTheme boardTheme = BoardTheme.blue; bool drawMode = true; bool pieceAnimation = true; @@ -129,6 +141,7 @@ class _HomePageState extends State { }); }, ), + pieceShiftMethod: pieceShiftMethod, ), data: BoardData( interactableSide: playMode == Mode.botPlay @@ -242,6 +255,30 @@ class _HomePageState extends State { ), ], ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + ElevatedButton( + child: Text( + 'Piece shift method: ${pieceShiftMethodLabel(pieceShiftMethod)}'), + onPressed: () => _showChoicesPicker( + context, + choices: PieceShiftMethod.values, + selectedItem: pieceShiftMethod, + labelBuilder: (t) => Text(pieceShiftMethodLabel(t)), + onSelectedItemChanged: (PieceShiftMethod? value) { + setState(() { + if (value != null) { + pieceShiftMethod = value; + } + }); + }, + ), + ), + const SizedBox(width: 8), + ], + ), if (playMode == Mode.freePlay) Center( child: IconButton( diff --git a/example/pubspec.lock b/example/pubspec.lock index 52e2fd6..8d33af3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -31,7 +31,7 @@ packages: path: ".." relative: true source: path - version: "3.1.2" + version: "3.2.0" clock: dependency: transitive description: diff --git a/lib/src/board_settings.dart b/lib/src/board_settings.dart index 9e5c2d3..3737c1f 100644 --- a/lib/src/board_settings.dart +++ b/lib/src/board_settings.dart @@ -5,6 +5,18 @@ import 'models.dart'; import 'piece_set.dart'; import 'draw_shape_options.dart'; +/// Describes how moves are made on an interactive board. +enum PieceShiftMethod { + /// First tap the piece to be moved, then tap the target square. + tapTwoSquares, + + /// Drag-and-drop the piece to the target square. + drag, + + /// Both tap and drag are enabled. + either; +} + /// Board settings that control the theme, behavior and purpose of the board. /// /// This is meant for fixed settings that don't change during a game. Sensible @@ -33,6 +45,7 @@ class BoardSettings { this.enablePremoveCastling = true, this.autoQueenPromotion = false, this.autoQueenPromotionOnPremove = true, + this.pieceShiftMethod = PieceShiftMethod.either, }); /// Theme of the board @@ -79,6 +92,9 @@ class BoardSettings { /// automatically to queen only if the premove is confirmed final bool autoQueenPromotionOnPremove; + /// Controls how moves are made. + final PieceShiftMethod pieceShiftMethod; + /// Shape drawing options object containing data about how new shapes can be drawn. final DrawShapeOptions drawShape; @@ -105,6 +121,7 @@ class BoardSettings { other.enablePremoveCastling == enablePremoveCastling && other.autoQueenPromotion == autoQueenPromotion && other.autoQueenPromotionOnPremove == autoQueenPromotionOnPremove && + other.pieceShiftMethod == pieceShiftMethod && other.drawShape == drawShape; } @@ -124,6 +141,7 @@ class BoardSettings { enablePremoveCastling, autoQueenPromotion, autoQueenPromotionOnPremove, + pieceShiftMethod, drawShape, ); @@ -142,6 +160,7 @@ class BoardSettings { bool? enablePremoveCastling, bool? autoQueenPromotion, bool? autoQueenPromotionOnPremove, + PieceShiftMethod? pieceShiftMethod, DrawShapeOptions? drawShape, }) { return BoardSettings( @@ -161,6 +180,7 @@ class BoardSettings { autoQueenPromotionOnPremove: autoQueenPromotionOnPremove ?? this.autoQueenPromotionOnPremove, autoQueenPromotion: autoQueenPromotion ?? this.autoQueenPromotion, + pieceShiftMethod: pieceShiftMethod ?? this.pieceShiftMethod, drawShape: drawShape ?? this.drawShape, ); } diff --git a/lib/src/widgets/board.dart b/lib/src/widgets/board.dart index 3992395..e59e414 100644 --- a/lib/src/widgets/board.dart +++ b/lib/src/widgets/board.dart @@ -557,6 +557,10 @@ class _BoardState extends State { _premoveDests = null; }); } + + if (widget.settings.pieceShiftMethod == PieceShiftMethod.drag) { + _shouldDeselectOnTapUp = true; + } } void _onPointerMove(PointerMoveEvent details) { @@ -577,7 +581,9 @@ class _BoardState extends State { } if (_currentPointerDownEvent == null || - _currentPointerDownEvent!.pointer != details.pointer) return; + _currentPointerDownEvent!.pointer != details.pointer || + widget.settings.pieceShiftMethod == PieceShiftMethod.tapTwoSquares) + return; final distance = (details.position - _currentPointerDownEvent!.position).distance; diff --git a/pubspec.yaml b/pubspec.yaml index ca191a8..4cd9c5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: chessground description: Chess board UI developed for lichess.org. It has no chess logic inside so it can be used for chess variants. -version: 3.1.2 +version: 3.2.0 repository: https://github.com/lichess-org/flutter-chessground funding: - https://lichess.org/patron diff --git a/test/widgets/board_test.dart b/test/widgets/board_test.dart index ae6aa32..6422de4 100644 --- a/test/widgets/board_test.dart +++ b/test/widgets/board_test.dart @@ -106,6 +106,33 @@ void main() { expect(find.byKey(const Key('e4-lastMove')), findsOneWidget); }); + testWidgets('Cannot move by tap if piece shift method is drag', + (WidgetTester tester) async { + await tester.pumpWidget( + buildBoard( + initialInteractableSide: InteractableSide.both, + pieceShiftMethod: PieceShiftMethod.drag, + ), + ); + await tester.tap(find.byKey(const Key('e2-whitePawn'))); + await tester.pump(); + + // Tapping a square should have no effect... + expect(find.byKey(const Key('e2-selected')), findsNothing); + expect(find.byType(MoveDest), findsNothing); + + // ... but move by drag should work + await tester.dragFrom( + squareOffset('e2'), + const Offset(0, -(squareSize * 2)), + ); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('e4-whitePawn')), findsOneWidget); + expect(find.byKey(const Key('e2-whitePawn')), findsNothing); + expect(find.byKey(const Key('e2-lastMove')), findsOneWidget); + expect(find.byKey(const Key('e4-lastMove')), findsOneWidget); + }); + testWidgets('castling by taping king then rook is possible', (WidgetTester tester) async { await tester.pumpWidget( @@ -170,6 +197,40 @@ void main() { expect(find.byKey(const Key('e4-lastMove')), findsOneWidget); }); + testWidgets('Cannot move by drag if piece shift method is tapTwoSquares', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + buildBoard( + initialInteractableSide: InteractableSide.both, + pieceShiftMethod: PieceShiftMethod.tapTwoSquares, + ), + ); + await tester.dragFrom( + squareOffset('e2'), + const Offset(0, -(squareSize * 2)), + ); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('e4-whitePawn')), findsNothing); + expect(find.byKey(const Key('e2-whitePawn')), findsOneWidget); + expect(find.byKey(const Key('e2-lastMove')), findsNothing); + expect(find.byKey(const Key('e4-lastMove')), findsNothing); + + // Original square is still selected after drag attempt + expect(find.byKey(const Key('e2-selected')), findsOneWidget); + expect(find.byType(MoveDest), findsNWidgets(2)); + + // ...so we can still tap to move + await tester.tapAt(squareOffset('e4')); + await tester.pump(); + expect(find.byKey(const Key('e2-selected')), findsNothing); + expect(find.byType(MoveDest), findsNothing); + expect(find.byKey(const Key('e4-whitePawn')), findsOneWidget); + expect(find.byKey(const Key('e2-whitePawn')), findsNothing); + expect(find.byKey(const Key('e2-lastMove')), findsOneWidget); + expect(find.byKey(const Key('e4-lastMove')), findsOneWidget); + }); + testWidgets( '2 simultaneous pointer down events will cancel current drag/selection', ( @@ -859,6 +920,7 @@ Widget buildBoard({ String initialFen = dc.kInitialFEN, ISet? initialShapes, bool enableDrawingShapes = false, + PieceShiftMethod pieceShiftMethod = PieceShiftMethod.either, /// play the first available move for the opponent after a delay of 200ms bool shouldPlayOpponentMove = false, @@ -884,6 +946,7 @@ Widget buildBoard({ }, newShapeColor: const Color(0xFF0000FF), ), + pieceShiftMethod: pieceShiftMethod, ); return Board(