Skip to content

Commit

Permalink
feat: add a BoardEditor widget
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-anders committed Jul 20, 2024
1 parent a079cfb commit e225abf
Show file tree
Hide file tree
Showing 12 changed files with 722 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.3.0

- Add a `BoardEditor` widget, intended to be used as the basis for a board editor like lichess.org/editor

## 3.2.0

- Add `pieceShiftMethod` to `BoardSetttings`, with possible values: `either` (default), `drag`, or `tapTwoSquares`.
Expand Down
159 changes: 159 additions & 0 deletions example/lib/board_editor_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import 'package:board_example/board_theme.dart';
import 'package:flutter/material.dart';
import 'package:chessground/chessground.dart';
import 'package:dartchess/dartchess.dart' as dc;
import 'package:collection/collection.dart';

class BoardEditorPage extends StatefulWidget {
const BoardEditorPage({super.key});

@override
State<StatefulWidget> createState() => _BoardEditorPageState();
}

class _BoardEditorPageState extends State<BoardEditorPage> {
Pieces pieces = readFen(dc.kInitialFEN);

Piece? pieceToAddOnTap;
bool deleteOnTap = false;

@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;

const PieceSet pieceSet = PieceSet.merida;

final settings = BoardEditorSettings(
pieceAssets: pieceSet.assets,
colorScheme: BoardTheme.blue.colors,
enableCoordinates: true,
);
final boardEditor = BoardEditor(
size: screenWidth,
orientation: Side.white,
pieces: pieces,
settings: settings,
onTappedSquare: (squareId) => setState(() {
if (deleteOnTap) {
pieces.remove(squareId);
} else if (pieceToAddOnTap != null) {
pieces[squareId] = pieceToAddOnTap!;
}
}),
onDiscardedPiece: (squareId) => setState(() {
pieces.remove(squareId);
}),
onDroppedPiece: (origin, destination, piece) => setState(() {
pieces[destination] = piece;
if (origin != null) {
pieces.remove(origin);
}
}),
);

makePieceMenu(side) => PieceMenu(
side: side,
pieceSet: pieceSet,
squareSize: boardEditor.squareSize,
settings: settings,
selectedPiece: pieceToAddOnTap,
pieceTapped: (role) => setState(() {
pieceToAddOnTap = Piece(role: role, color: side);
deleteOnTap = false;
}),
deleteSelected: deleteOnTap,
deleteTapped: () => setState(() {
pieceToAddOnTap = null;
deleteOnTap = !deleteOnTap;
}),
);

return Scaffold(
appBar: AppBar(
title: const Text('Board Editor'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
makePieceMenu(Side.white),
boardEditor,
makePieceMenu(Side.black),
Text('FEN: ${writeFen(pieces)}'),
],
),
),
);
}
}

class PieceMenu extends StatelessWidget {
const PieceMenu({
super.key,
required this.side,
required this.pieceSet,
required this.squareSize,
required this.selectedPiece,
required this.deleteSelected,
required this.settings,
required this.pieceTapped,
required this.deleteTapped,
});

final Side side;
final PieceSet pieceSet;
final double squareSize;
final Piece? selectedPiece;
final bool deleteSelected;
final BoardEditorSettings settings;
final Function(Role role) pieceTapped;
final Function() deleteTapped;

@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
...Role.values.mapIndexed(
(i, role) {
final piece = Piece(role: role, color: side);
final pieceWidget = PieceWidget(
piece: piece,
size: squareSize,
pieceAssets: pieceSet.assets,
);

return Container(
color:
selectedPiece == piece ? Colors.blue : Colors.transparent,
child: GestureDetector(
onTap: () => pieceTapped(role),
child: Draggable(
data: piece,
feedback: PieceDragFeedback(
piece: piece,
pieceAssets: pieceSet.assets,
squareSize: squareSize,
),
child: pieceWidget),
),
);
},
).toList(),
Container(
color: deleteSelected ? Colors.red : Colors.transparent,
child: GestureDetector(
onTap: () => deleteTapped(),
child: Icon(
Icons.delete,
size: squareSize,
),
),
),
],
),
);
}
}
20 changes: 16 additions & 4 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:math';
import 'package:board_example/board_editor_page.dart';
import 'package:flutter/material.dart';
import 'package:chessground/chessground.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
Expand Down Expand Up @@ -77,10 +78,10 @@ class _HomePageState extends State<HomePage> {

return Scaffold(
appBar: AppBar(
title: playMode == Mode.botPlay
? const Text('Random Bot')
: const Text('Free Play'),
),
title: switch (playMode) {
Mode.botPlay => const Text('Random Bot'),
Mode.freePlay => const Text('Free Play'),
}),
drawer: Drawer(
child: ListView(
children: [
Expand All @@ -105,6 +106,17 @@ class _HomePageState extends State<HomePage> {
Navigator.pop(context);
},
),
ListTile(
title: const Text('Board Editor'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BoardEditorPage(),
),
);
},
),
ListTile(
title: const Text('Board Thumbnails'),
onTap: () {
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ packages:
path: ".."
relative: true
source: path
version: "3.2.0"
version: "3.3.0"
clock:
dependency: transitive
description:
Expand Down
3 changes: 3 additions & 0 deletions lib/chessground.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ library chessground;
export 'src/board_color_scheme.dart';
export 'src/draw_shape_options.dart';
export 'src/board_data.dart';
export 'src/board_editor_settings.dart';
export 'src/board_settings.dart';
export 'src/fen.dart';
export 'src/models.dart';
export 'src/piece_set.dart';
export 'src/premove.dart';
export 'src/widgets/board.dart';
export 'src/widgets/board_editor.dart';
export 'src/widgets/drag.dart';
export 'src/widgets/highlight.dart';
export 'src/widgets/piece.dart';
export 'src/widgets/background.dart';
94 changes: 94 additions & 0 deletions lib/src/board_editor_settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:flutter/widgets.dart';

import 'board_color_scheme.dart';
import 'models.dart';
import 'piece_set.dart';

/// Board editor settings that control the theme, behavior and purpose of the board editor.
///
/// This is meant for fixed settings that don't change while editing the board. Sensible
/// defaults are provided.
@immutable
class BoardEditorSettings {
const BoardEditorSettings({
// theme
this.colorScheme = BoardColorScheme.brown,
this.pieceAssets = PieceSet.cburnettAssets,
// visual settings
this.borderRadius = BorderRadius.zero,
this.boxShadow = const <BoxShadow>[],
this.enableCoordinates = true,
this.dragFeedbackSize = 2.0,
this.dragFeedbackOffset = const Offset(0.0, -1.0),
});

/// Theme of the board
final BoardColorScheme colorScheme;

/// Piece set
final PieceAssets pieceAssets;

/// Border radius of the board
final BorderRadiusGeometry borderRadius;

/// Box shadow of the board
final List<BoxShadow> boxShadow;

/// Whether to show board coordinates
final bool enableCoordinates;

// Scale up factor for the piece currently under drag
final double dragFeedbackSize;

// Offset for the piece currently under drag
final Offset dragFeedbackOffset;

@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is BoardEditorSettings &&
other.colorScheme == colorScheme &&
other.pieceAssets == pieceAssets &&
other.borderRadius == borderRadius &&
other.boxShadow == boxShadow &&
other.enableCoordinates == enableCoordinates &&
other.dragFeedbackSize == dragFeedbackSize &&
other.dragFeedbackOffset == dragFeedbackOffset;
}

@override
int get hashCode => Object.hash(
colorScheme,
pieceAssets,
borderRadius,
boxShadow,
enableCoordinates,
dragFeedbackSize,
dragFeedbackOffset,
);

BoardEditorSettings copyWith({
BoardColorScheme? colorScheme,
PieceAssets? pieceAssets,
BorderRadiusGeometry? borderRadius,
List<BoxShadow>? boxShadow,
bool? enableCoordinates,
double? dragFeedbackSize,
Offset? dragFeedbackOffset,
}) {
return BoardEditorSettings(
colorScheme: colorScheme ?? this.colorScheme,
pieceAssets: pieceAssets ?? this.pieceAssets,
borderRadius: borderRadius ?? this.borderRadius,
boxShadow: boxShadow ?? this.boxShadow,
enableCoordinates: enableCoordinates ?? this.enableCoordinates,
dragFeedbackSize: dragFeedbackSize ?? this.dragFeedbackSize,
dragFeedbackOffset: dragFeedbackOffset ?? this.dragFeedbackOffset,
);
}
}
33 changes: 33 additions & 0 deletions lib/src/fen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,39 @@ Pieces readFen(String fen) {
return pieces;
}

/// Convert the pieces to the board part of a FEN string
String writeFen(Pieces pieces) {
final buffer = StringBuffer();
int empty = 0;
for (int rank = 7; rank >= 0; rank--) {
for (int file = 0; file < 8; file++) {
final piece = pieces[Coord(x: file, y: rank).squareId];
if (piece == null) {
empty++;
} else {
if (empty > 0) {
buffer.write(empty.toString());
empty = 0;
}
buffer.write(
piece.color == Side.white
? piece.role.letter.toUpperCase()
: piece.role.letter.toLowerCase(),
);
}

if (file == 7) {
if (empty > 0) {
buffer.write(empty.toString());
empty = 0;
}
if (rank != 0) buffer.write('/');
}
}
}
return buffer.toString();
}

const _roles = {
'p': Role.pawn,
'r': Role.rook,
Expand Down
Loading

0 comments on commit e225abf

Please sign in to comment.