浏览代码

chore: update ui

appflowy 2 年之前
父节点
当前提交
db5b3e3bd3
共有 20 个文件被更改,包括 616 次插入191 次删除
  1. 5 0
      frontend/app_flowy/packages/appflowy_board/CHANGELOG.md
  2. 58 36
      frontend/app_flowy/packages/appflowy_board/README.md
  3. 二进制
      frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif
  4. 3 3
      frontend/app_flowy/packages/appflowy_board/example/lib/main.dart
  5. 62 49
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  6. 1 0
      frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart
  7. 1 1
      frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
  8. 62 24
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  9. 36 12
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
  10. 12 6
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
  11. 8 5
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  12. 92 4
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
  13. 10 10
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart
  14. 34 31
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
  15. 54 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart
  16. 18 10
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
  17. 3 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart
  18. 37 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart
  19. 46 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart
  20. 74 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart

+ 5 - 0
frontend/app_flowy/packages/appflowy_board/CHANGELOG.md

@@ -1,3 +1,8 @@
+# 0.0.3
+* Support customize UI
+* Update example
+* Add AppFlowy style widget
+
 ## 0.0.2
 
 * Update documentation

+ 58 - 36
frontend/app_flowy/packages/appflowy_board/README.md

@@ -6,30 +6,25 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co
 **appflowy_board** will be a standard git repository when it becomes stable.
 ## Getting Started
 
+<p>
+<img src="" width="180" title="AppFlowyBoard">
+</p>
 
 ```dart
 @override
   void initState() {
-    final column1 = BoardColumnData(id: "1", items: [
-      TextItem("a"),
-      TextItem("b"),
-      TextItem("c"),
-      TextItem("d"),
+    final column1 = BoardColumnData(id: "To Do", items: [
+      TextItem("Card 1"),
+      TextItem("Card 2"),
+      TextItem("Card 3"),
+      TextItem("Card 4"),
     ]);
-    final column2 = BoardColumnData(id: "2", items: [
-      TextItem("1"),
-      TextItem("2"),
-      TextItem("3"),
-      TextItem("4"),
-      TextItem("5"),
+    final column2 = BoardColumnData(id: "In Progress", items: [
+      TextItem("Card 5"),
+      TextItem("Card 6"),
     ]);
 
-    final column3 = BoardColumnData(id: "3", items: [
-      TextItem("A"),
-      TextItem("B"),
-      TextItem("C"),
-      TextItem("D"),
-    ]);
+    final column3 = BoardColumnData(id: "Done", items: []);
 
     boardDataController.addColumn(column1);
     boardDataController.addColumn(column2);
@@ -40,25 +35,52 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co
 
   @override
   Widget build(BuildContext context) {
-    return Board(
-      dataController: boardDataController,
-      background: Container(color: Colors.red),
-      footBuilder: (context, columnData) {
-        return Container(
-          color: Colors.purple,
-          height: 30,
-        );
-      },
-      headerBuilder: (context, columnData) {
-        return Container(
-          color: Colors.yellow,
-          height: 30,
-        );
-      },
-      cardBuilder: (context, item) {
-        return _RowWidget(item: item as TextItem, key: ObjectKey(item));
-      },
-      columnConstraints: const BoxConstraints.tightFor(width: 240),
+    final config = BoardConfig(
+      columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+    );
+    return Container(
+      color: Colors.white,
+      child: Padding(
+        padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+        child: Board(
+          dataController: boardDataController,
+          footBuilder: (context, columnData) {
+            return AppFlowyColumnFooter(
+              icon: const Icon(Icons.add, size: 20),
+              title: const Text('New'),
+              height: 50,
+              margin: config.columnItemPadding,
+            );
+          },
+          headerBuilder: (context, columnData) {
+            return AppFlowyColumnHeader(
+              icon: const Icon(Icons.lightbulb_circle),
+              title: Text(columnData.id),
+              addIcon: const Icon(Icons.add, size: 20),
+              moreIcon: const Icon(Icons.more_horiz, size: 20),
+              height: 50,
+              margin: config.columnItemPadding,
+            );
+          },
+          cardBuilder: (context, item) {
+            final textItem = item as TextItem;
+            return AppFlowyColumnItemCard(
+              key: ObjectKey(item),
+              child: Align(
+                alignment: Alignment.centerLeft,
+                child: Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 20),
+                  child: Text(textItem.s),
+                ),
+              ),
+            );
+          },
+          columnConstraints: const BoxConstraints.tightFor(width: 240),
+          config: BoardConfig(
+            columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+          ),
+        ),
+      ),
     );
   }
 ```

二进制
frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif


+ 3 - 3
frontend/app_flowy/packages/appflowy_board/example/lib/main.dart

@@ -32,7 +32,7 @@ class _MyAppState extends State<MyApp> {
     return MaterialApp(
       home: Scaffold(
           appBar: AppBar(
-            title: const Text('FlowyBoard example'),
+            title: const Text('AppFlowy Board'),
           ),
           body: _examples[_currentIndex],
           bottomNavigationBar: BottomNavigationBar(
@@ -43,10 +43,10 @@ class _MyAppState extends State<MyApp> {
             items: [
               BottomNavigationBarItem(
                   icon: Icon(Icons.grid_on, color: _bottomNavigationColor),
-                  label: "MultiBoardList"),
+                  label: "MultiColumn"),
               BottomNavigationBarItem(
                   icon: Icon(Icons.grid_on, color: _bottomNavigationColor),
-                  label: "SingleBoardList"),
+                  label: "SingleColumn"),
             ],
             onTap: (int index) {
               setState(() {

+ 62 - 49
frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart

@@ -23,26 +23,18 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
 
   @override
   void initState() {
-    final column1 = BoardColumnData(id: "1", items: [
-      TextItem("a"),
-      TextItem("b"),
-      TextItem("c"),
-      TextItem("d"),
+    final column1 = BoardColumnData(id: "To Do", items: [
+      TextItem("Card 1"),
+      TextItem("Card 2"),
+      TextItem("Card 3"),
+      TextItem("Card 4"),
     ]);
-    final column2 = BoardColumnData(id: "2", items: [
-      TextItem("1"),
-      TextItem("2"),
-      TextItem("3"),
-      TextItem("4"),
-      TextItem("5"),
+    final column2 = BoardColumnData(id: "In Progress", items: [
+      TextItem("Card 5"),
+      TextItem("Card 6"),
     ]);
 
-    final column3 = BoardColumnData(id: "3", items: [
-      TextItem("A"),
-      TextItem("B"),
-      TextItem("C"),
-      TextItem("D"),
-    ]);
+    final column3 = BoardColumnData(id: "Done", items: []);
 
     boardDataController.addColumn(column1);
     boardDataController.addColumn(column2);
@@ -53,40 +45,52 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
 
   @override
   Widget build(BuildContext context) {
-    return Board(
-      dataController: boardDataController,
-      background: Container(color: Colors.red),
-      footBuilder: (context, columnData) {
-        return Container(
-          color: Colors.purple,
-          height: 30,
-        );
-      },
-      headerBuilder: (context, columnData) {
-        return Container(
-          color: Colors.yellow,
-          height: 30,
-        );
-      },
-      cardBuilder: (context, item) {
-        return _RowWidget(item: item as TextItem, key: ObjectKey(item));
-      },
-      columnConstraints: const BoxConstraints.tightFor(width: 240),
+    final config = BoardConfig(
+      columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
     );
-  }
-}
-
-class _RowWidget extends StatelessWidget {
-  final TextItem item;
-  const _RowWidget({Key? key, required this.item}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
     return Container(
-      key: ObjectKey(item),
-      height: 60,
-      color: Colors.green,
-      child: Center(child: Text(item.s)),
+      color: Colors.white,
+      child: Padding(
+        padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+        child: Board(
+          dataController: boardDataController,
+          footBuilder: (context, columnData) {
+            return AppFlowyColumnFooter(
+              icon: const Icon(Icons.add, size: 20),
+              title: const Text('New'),
+              height: 50,
+              margin: config.columnItemPadding,
+            );
+          },
+          headerBuilder: (context, columnData) {
+            return AppFlowyColumnHeader(
+              icon: const Icon(Icons.lightbulb_circle),
+              title: Text(columnData.id),
+              addIcon: const Icon(Icons.add, size: 20),
+              moreIcon: const Icon(Icons.more_horiz, size: 20),
+              height: 50,
+              margin: config.columnItemPadding,
+            );
+          },
+          cardBuilder: (context, item) {
+            final textItem = item as TextItem;
+            return AppFlowyColumnItemCard(
+              key: ObjectKey(item),
+              child: Align(
+                alignment: Alignment.centerLeft,
+                child: Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 20),
+                  child: Text(textItem.s),
+                ),
+              ),
+            );
+          },
+          columnConstraints: const BoxConstraints.tightFor(width: 240),
+          config: BoardConfig(
+            columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+          ),
+        ),
+      ),
     );
   }
 }
@@ -99,3 +103,12 @@ class TextItem extends ColumnItem {
   @override
   String get id => s;
 }
+
+extension HexColor on Color {
+  static Color fromHex(String hexString) {
+    final buffer = StringBuffer();
+    if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
+    buffer.write(hexString.replaceFirst('#', ''));
+    return Color(int.parse(buffer.toString(), radix: 16));
+  }
+}

+ 1 - 0
frontend/app_flowy/packages/appflowy_board/lib/appflowy_board.dart

@@ -2,4 +2,5 @@ library appflowy_board;
 
 export 'src/widgets/board_column/board_column_data.dart';
 export 'src/widgets/board_data.dart';
+export 'src/widgets/styled_widgets/appflowy_styled_widgets.dart';
 export 'src/widgets/board.dart';

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart

@@ -6,7 +6,7 @@ const DART_LOG = "Dart_LOG";
 class Log {
   // static const enableLog = bool.hasEnvironment(DART_LOG);
   // static final shared = Log();
-  static const enableLog = false;
+  static const enableLog = true;
 
   static void info(String? message) {
     if (enableLog) {

+ 62 - 24
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -3,23 +3,29 @@ import 'package:provider/provider.dart';
 import 'board_column/board_column.dart';
 import 'board_column/board_column_data.dart';
 import 'board_data.dart';
-import 'flex/drag_target_inteceptor.dart';
-import 'flex/reorder_flex.dart';
-import 'phantom/phantom_controller.dart';
+import 'reorder_flex/drag_target_inteceptor.dart';
+import 'reorder_flex/reorder_flex.dart';
+import 'reorder_phantom/phantom_controller.dart';
 import '../rendering/board_overlay.dart';
 
+class BoardConfig {
+  final double cornerRadius;
+  final EdgeInsets columnPadding;
+  final EdgeInsets columnItemPadding;
+  final Color columnBackgroundColor;
+
+  const BoardConfig({
+    this.cornerRadius = 6.0,
+    this.columnPadding = const EdgeInsets.symmetric(horizontal: 8),
+    this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 10),
+    this.columnBackgroundColor = Colors.transparent,
+  });
+}
+
 class Board extends StatelessWidget {
   /// The direction to use as the main axis.
   final Axis direction = Axis.vertical;
 
-  /// How much space to place between children in a run in the main axis.
-  /// Defaults to 10.0.
-  final double spacing;
-
-  /// How much space to place between the runs themselves in the cross axis.
-  /// Defaults to 0.0.
-  final double runSpacing;
-
   ///
   final Widget? background;
 
@@ -40,15 +46,16 @@ class Board extends StatelessWidget {
   ///
   final BoardPhantomController phantomController;
 
+  final BoardConfig config;
+
   Board({
     required this.dataController,
     required this.cardBuilder,
-    this.spacing = 10.0,
-    this.runSpacing = 0.0,
     this.background,
     this.footBuilder,
     this.headerBuilder,
     this.columnConstraints = const BoxConstraints(maxWidth: 200),
+    this.config = const BoardConfig(),
     Key? key,
   })  : phantomController = BoardPhantomController(delegate: dataController),
         super(key: key);
@@ -60,9 +67,9 @@ class Board extends StatelessWidget {
       child: Consumer<BoardDataController>(
         builder: (context, notifier, child) {
           return BoardContent(
+            config: config,
             dataController: dataController,
             background: background,
-            spacing: spacing,
             delegate: phantomController,
             columnConstraints: columnConstraints,
             cardBuilder: cardBuilder,
@@ -84,8 +91,8 @@ class BoardContent extends StatefulWidget {
   final OnDragEnded? onDragEnded;
   final BoardDataController dataController;
   final Widget? background;
-  final double spacing;
-  final ReorderFlexConfig config;
+  final BoardConfig config;
+  final ReorderFlexConfig reorderFlexConfig;
   final BoxConstraints columnConstraints;
 
   ///
@@ -101,7 +108,8 @@ class BoardContent extends StatefulWidget {
 
   final BoardPhantomController phantomController;
 
-  BoardContent({
+  const BoardContent({
+    required this.config,
     required this.onReorder,
     required this.delegate,
     required this.dataController,
@@ -109,14 +117,13 @@ class BoardContent extends StatefulWidget {
     this.onDragEnded,
     this.scrollController,
     this.background,
-    this.spacing = 10.0,
     required this.columnConstraints,
     required this.cardBuilder,
     this.footBuilder,
     this.headerBuilder,
     required this.phantomController,
     Key? key,
-  })  : config = ReorderFlexConfig(spacing: spacing),
+  })  : reorderFlexConfig = const ReorderFlexConfig(),
         super(key: key);
 
   @override
@@ -140,7 +147,7 @@ class _BoardContentState extends State<BoardContent> {
 
         final reorderFlex = ReorderFlex(
           key: widget.key,
-          config: widget.config,
+          config: widget.reorderFlexConfig,
           scrollController: widget.scrollController,
           onDragStarted: widget.onDragStarted,
           onReorder: widget.onReorder,
@@ -154,7 +161,15 @@ class _BoardContentState extends State<BoardContent> {
         return Stack(
           alignment: AlignmentDirectional.topStart,
           children: [
-            if (widget.background != null) widget.background!,
+            if (widget.background != null)
+              Container(
+                clipBehavior: Clip.hardEdge,
+                decoration: BoxDecoration(
+                  borderRadius:
+                      BorderRadius.circular(widget.config.cornerRadius),
+                ),
+                child: widget.background,
+              ),
             reorderFlex,
           ],
         );
@@ -173,8 +188,12 @@ class _BoardContentState extends State<BoardContent> {
   }
 
   List<Widget> _buildColumns() {
-    final List<Widget> children = widget.dataController.columnDatas.map(
-      (columnData) {
+    final List<Widget> children =
+        widget.dataController.columnDatas.asMap().entries.map(
+      (item) {
+        final columnData = item.value;
+        final columnIndex = item.key;
+
         final dataSource = _BoardColumnDataSourceImpl(
           columnId: columnData.id,
           dataController: widget.dataController,
@@ -188,6 +207,8 @@ class _BoardContentState extends State<BoardContent> {
               return ConstrainedBox(
                 constraints: widget.columnConstraints,
                 child: BoardColumnWidget(
+                  margin: _marginFromIndex(columnIndex),
+                  itemMargin: widget.config.columnItemPadding,
                   headerBuilder: widget.headerBuilder,
                   footBuilder: widget.footBuilder,
                   cardBuilder: widget.cardBuilder,
@@ -195,7 +216,8 @@ class _BoardContentState extends State<BoardContent> {
                   scrollController: ScrollController(),
                   phantomController: widget.phantomController,
                   onReorder: widget.dataController.moveColumnItem,
-                  spacing: 10,
+                  cornerRadius: widget.config.cornerRadius,
+                  backgroundColor: widget.config.columnBackgroundColor,
                 ),
               );
             },
@@ -206,6 +228,22 @@ class _BoardContentState extends State<BoardContent> {
 
     return children;
   }
+
+  EdgeInsets _marginFromIndex(int index) {
+    if (widget.dataController.columnDatas.isEmpty) {
+      return widget.config.columnPadding;
+    }
+
+    if (index == 0) {
+      return EdgeInsets.only(right: widget.config.columnPadding.right);
+    }
+
+    if (index == widget.dataController.columnDatas.length - 1) {
+      return EdgeInsets.only(left: widget.config.columnPadding.left);
+    }
+
+    return widget.config.columnPadding;
+  }
 }
 
 class _BoardColumnDataSourceImpl extends BoardColumnDataDataSource {

+ 36 - 12
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart

@@ -3,9 +3,9 @@ import 'dart:collection';
 import 'package:flutter/material.dart';
 import '../../rendering/board_overlay.dart';
 import '../../utils/log.dart';
-import '../phantom/phantom_controller.dart';
-import '../flex/reorder_flex.dart';
-import '../flex/drag_target_inteceptor.dart';
+import '../reorder_phantom/phantom_controller.dart';
+import '../reorder_flex/reorder_flex.dart';
+import '../reorder_flex/drag_target_inteceptor.dart';
 import 'board_column_data.dart';
 
 typedef OnColumnDragStarted = void Function(int index);
@@ -79,7 +79,15 @@ class BoardColumnWidget extends StatefulWidget {
 
   final BoardColumnFooterBuilder? footBuilder;
 
-  BoardColumnWidget({
+  final EdgeInsets margin;
+
+  final EdgeInsets itemMargin;
+
+  final double cornerRadius;
+
+  final Color backgroundColor;
+
+  const BoardColumnWidget({
     Key? key,
     this.headerBuilder,
     this.footBuilder,
@@ -90,8 +98,11 @@ class BoardColumnWidget extends StatefulWidget {
     this.onDragStarted,
     this.scrollController,
     this.onDragEnded,
-    double? spacing,
-  })  : config = ReorderFlexConfig(spacing: spacing),
+    this.margin = EdgeInsets.zero,
+    this.itemMargin = EdgeInsets.zero,
+    this.cornerRadius = 0.0,
+    this.backgroundColor = Colors.transparent,
+  })  : config = const ReorderFlexConfig(),
         super(key: key);
 
   @override
@@ -149,12 +160,25 @@ class _BoardColumnWidgetState extends State<BoardColumnWidget> {
           children: children,
         );
 
-        return Column(
-          children: [
-            if (header != null) header,
-            Expanded(child: reorderFlex),
-            if (footer != null) footer,
-          ],
+        return Container(
+          margin: widget.margin,
+          clipBehavior: Clip.hardEdge,
+          decoration: BoxDecoration(
+            color: widget.backgroundColor,
+            borderRadius: BorderRadius.circular(widget.cornerRadius),
+          ),
+          child: Column(
+            children: [
+              if (header != null) header,
+              Expanded(
+                child: Padding(
+                  padding: widget.itemMargin,
+                  child: reorderFlex,
+                ),
+              ),
+              if (footer != null) footer,
+            ],
+          ),
         );
       },
       opaque: false,

+ 12 - 6
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart

@@ -3,7 +3,7 @@ import 'dart:collection';
 import 'package:equatable/equatable.dart';
 import 'package:flutter/material.dart';
 import '../../utils/log.dart';
-import '../flex/reorder_flex.dart';
+import '../reorder_flex/reorder_flex.dart';
 
 abstract class ColumnItem extends ReoderFlexItem {
   bool get isPhantom => false;
@@ -92,10 +92,16 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
 
   /// Replace the item at index with the [newItem].
   void replace(int index, ColumnItem newItem) {
-    final removedItem = columnData._items.removeAt(index);
-    columnData._items.insert(index, newItem);
-    Log.debug(
-        '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
+    if (columnData._items.isEmpty) {
+      columnData._items.add(newItem);
+      Log.debug('[$BoardColumnDataController] $columnData add $newItem');
+    } else {
+      final removedItem = columnData._items.removeAt(index);
+      columnData._items.insert(index, newItem);
+      Log.debug(
+          '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
+    }
+
     notifyListeners();
   }
 }
@@ -119,6 +125,6 @@ class BoardColumnData extends ReoderFlexItem with EquatableMixin {
 
   @override
   String toString() {
-    return 'Column$id';
+    return 'Column:[$id]';
   }
 }

+ 8 - 5
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -4,9 +4,9 @@ import 'package:equatable/equatable.dart';
 
 import '../utils/log.dart';
 import 'board_column/board_column_data.dart';
-import 'flex/reorder_flex.dart';
+import 'reorder_flex/reorder_flex.dart';
 import 'package:flutter/material.dart';
-import 'phantom/phantom_controller.dart';
+import 'reorder_phantom/phantom_controller.dart';
 
 typedef OnMoveColumn = void Function(int fromIndex, int toIndex);
 
@@ -79,8 +79,11 @@ class BoardDataController extends ChangeNotifier
     int toColumnIndex,
   ) {
     final item = columnController(fromColumnId).removeAt(fromColumnIndex);
-    assert(
-        columnController(toColumnId).items[toColumnIndex] is PhantomColumnItem);
+
+    if (columnController(toColumnId).items.length > toColumnIndex) {
+      assert(columnController(toColumnId).items[toColumnIndex]
+          is PhantomColumnItem);
+    }
 
     columnController(toColumnId).replace(toColumnIndex, item);
 
@@ -120,7 +123,7 @@ class BoardDataController extends ChangeNotifier
       columnController.removeAt(index);
 
       Log.debug(
-          '[$BoardDataController] Column$columnId remove phantom, current count: ${columnController.items.length}');
+          '[$BoardDataController] Column:[$columnId] remove phantom, current count: ${columnController.items.length}');
     }
     return isExist;
   }

+ 92 - 4
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart

@@ -1,4 +1,6 @@
 import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:provider/provider.dart';
 import '../transitions.dart';
 
 abstract class DragTargetData {
@@ -65,6 +67,8 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
   final AnimationController insertAnimationController;
   final AnimationController deleteAnimationController;
 
+  final bool useMoveAnimation;
+
   ReorderDragTarget({
     Key? key,
     required this.child,
@@ -74,6 +78,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
     required this.onWillAccept,
     required this.insertAnimationController,
     required this.deleteAnimationController,
+    required this.useMoveAnimation,
     this.onAccept,
     this.onLeave,
     this.draggableTargetBuilder,
@@ -140,7 +145,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
           data: widget.dragTargetData,
           ignoringFeedbackSemantics: false,
           feedback: feedbackBuilder,
-          childWhenDragging: IgnorePointerWidget(child: widget.child),
+          childWhenDragging: IgnorePointerWidget(
+            useIntrinsicSize: !widget.useMoveAnimation,
+            child: widget.child,
+          ),
           onDragStarted: () {
             _draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size;
             widget.onDragStarted(
@@ -174,11 +182,13 @@ class _ReorderDragTargetState<T extends DragTargetData>
       transform: Matrix4.rotationZ(0),
       alignment: FractionalOffset.topLeft,
       child: Material(
-        elevation: 3.0,
         color: Colors.transparent,
         borderRadius: BorderRadius.zero,
         clipBehavior: Clip.hardEdge,
-        child: ConstrainedBox(constraints: constraints, child: child),
+        child: ConstrainedBox(
+          constraints: constraints,
+          child: Opacity(opacity: 0.6, child: child),
+        ),
       ),
     );
   }
@@ -254,10 +264,12 @@ class IgnorePointerWidget extends StatelessWidget {
     final sizedChild = useIntrinsicSize
         ? child
         : SizedBox(width: 0.0, height: 0.0, child: child);
+
+    final opacity = useIntrinsicSize ? 0.3 : 0.0;
     return IgnorePointer(
       ignoring: true,
       child: Opacity(
-        opacity: 0,
+        opacity: opacity,
         child: sizedChild,
       ),
     );
@@ -282,6 +294,82 @@ class PhantomWidget extends StatelessWidget {
   }
 }
 
+abstract class DragTargetMovePlaceholderDelegate {
+  void registerPlaceholder(
+    int dragTargetIndex,
+    void Function(int currentDragTargetIndex) callback,
+  );
+
+  void unregisterPlaceholder(int dragTargetIndex);
+}
+
+class DragTargeMovePlaceholder extends StatefulWidget {
+  final double height;
+  final Color color;
+  final Color highlightColor;
+  final int dragTargetIndex;
+  final DragTargetMovePlaceholderDelegate delegate;
+
+  const DragTargeMovePlaceholder({
+    required this.delegate,
+    required this.dragTargetIndex,
+    this.height = 4,
+    this.color = Colors.transparent,
+    this.highlightColor = Colors.lightBlue,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<DragTargeMovePlaceholder> createState() =>
+      _DragTargeMovePlaceholderState();
+}
+
+class _DragTargeMovePlaceholderState extends State<DragTargeMovePlaceholder> {
+  ValueNotifier<bool> isHighlight = ValueNotifier(false);
+
+  @override
+  void initState() {
+    widget.delegate.registerPlaceholder(
+      widget.dragTargetIndex,
+      (currentDragTargetIndex) {
+        if (!mounted) return;
+
+        SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
+          if (currentDragTargetIndex == -1) {
+            isHighlight.value = false;
+          } else {
+            isHighlight.value =
+                widget.dragTargetIndex == currentDragTargetIndex;
+          }
+        });
+      },
+    );
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    isHighlight.dispose();
+    widget.delegate.unregisterPlaceholder(widget.dragTargetIndex);
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: isHighlight,
+      child: Consumer<ValueNotifier<bool>>(
+        builder: (context, notifier, child) {
+          return Container(
+            height: widget.height,
+            color: notifier.value ? widget.highlightColor : widget.color,
+          );
+        },
+      ),
+    );
+  }
+}
+
 abstract class FakeDragTargetEventTrigger {
   void fakeOnDragEnded(VoidCallback callback);
 }

+ 10 - 10
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart

@@ -30,12 +30,14 @@ abstract class DragTargetInterceptor {
 }
 
 abstract class OverlapDragTargetDelegate {
-  void didReturnOriginalDragTarget();
-  void didCrossOtherDragTarget(
+  void cancel();
+  void moveTo(
     String reorderFlexId,
     FlexDragTargetData dragTargetData,
     int dragTargetIndex,
   );
+
+  bool canMoveTo(String dragTargetId);
 }
 
 /// [OverlappingDragTargetInteceptor] is used to receive the overlapping
@@ -68,13 +70,11 @@ class OverlappingDragTargetInteceptor extends DragTargetInterceptor {
       required String dragTargetId,
       required int dragTargetIndex}) {
     if (dragTargetId == dragTargetData.reorderFlexId) {
-      delegate.didReturnOriginalDragTarget();
+      delegate.cancel();
     } else {
-      delegate.didCrossOtherDragTarget(
-        dragTargetId,
-        dragTargetData,
-        dragTargetIndex,
-      );
+      if (delegate.canMoveTo(dragTargetId)) {
+        delegate.moveTo(dragTargetId, dragTargetData, 0);
+      }
     }
 
     return true;
@@ -128,13 +128,13 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
   @override
   void onAccept(FlexDragTargetData dragTargetData) {
     Log.trace(
-        '[$CrossReorderFlexDragTargetInterceptor] Column$reorderFlexId on onAccept');
+        '[$CrossReorderFlexDragTargetInterceptor] Column:[$reorderFlexId] on onAccept');
   }
 
   @override
   void onLeave(FlexDragTargetData dragTargetData) {
     Log.trace(
-        '[$CrossReorderFlexDragTargetInterceptor] Column$reorderFlexId on leave');
+        '[$CrossReorderFlexDragTargetInterceptor] Column:[$reorderFlexId] on leave');
   }
 
   @override

+ 34 - 31
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart

@@ -41,16 +41,19 @@ class ReorderFlexConfig {
   // How long an animation to scroll to an off-screen element
   final Duration scrollAnimationDuration = const Duration(milliseconds: 250);
 
-  final double? spacing;
+  final bool useMoveAnimation;
 
-  const ReorderFlexConfig({this.spacing});
+  final bool useMovePlaceholder;
+
+  const ReorderFlexConfig({
+    this.useMoveAnimation = true,
+  }) : useMovePlaceholder = !useMoveAnimation;
 }
 
 class ReorderFlex extends StatefulWidget {
   final ReorderFlexConfig config;
 
   final List<Widget> children;
-  final EdgeInsets? padding;
 
   /// [direction] How to place the children, default is Axis.vertical
   final Axis direction;
@@ -81,7 +84,6 @@ class ReorderFlex extends StatefulWidget {
     this.onDragStarted,
     this.onDragEnded,
     this.interceptor,
-    this.padding,
     this.direction = Axis.vertical,
   }) : super(key: key);
 
@@ -108,8 +110,11 @@ class ReorderFlexState extends State<ReorderFlex>
   /// [_animation] controls the dragging animations
   late DragTargetAnimation _animation;
 
+  late ReorderFlexNotifier _notifier;
+
   @override
   void initState() {
+    _notifier = ReorderFlexNotifier();
     dragState = DraggingState(widget.reorderFlexId);
 
     _animation = DragTargetAnimation(
@@ -154,13 +159,14 @@ class ReorderFlexState extends State<ReorderFlex>
 
     for (int i = 0; i < widget.children.length; i += 1) {
       Widget child = widget.children[i];
-
-      if (widget.config.spacing != null) {
-        children.add(SizedBox(width: widget.config.spacing!));
-      }
-
-      final wrapChild = _wrap(child, i);
-      children.add(wrapChild);
+      children.add(_wrap(child, i));
+
+      // if (widget.config.useMovePlaceholder) {
+      //   children.add(DragTargeMovePlaceholder(
+      //     dragTargetIndex: i,
+      //     delegate: _notifier,
+      //   ));
+      // }
     }
 
     final child = _wrapContainer(children);
@@ -199,7 +205,8 @@ class ReorderFlexState extends State<ReorderFlex>
   /// [childIndex]: the index of the child in a list
   Widget _wrap(Widget child, int childIndex) {
     return Builder(builder: (context) {
-      final dragTarget = _buildDragTarget(context, child, childIndex);
+      final ReorderDragTarget dragTarget =
+          _buildDragTarget(context, child, childIndex);
       int shiftedIndex = childIndex;
 
       if (dragState.isOverlapWithPhantom()) {
@@ -207,7 +214,7 @@ class ReorderFlexState extends State<ReorderFlex>
       }
 
       Log.trace(
-          'Rebuild: Column${dragState.id} ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
+          'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex');
       final currentIndex = dragState.currentIndex;
       final dragPhantomIndex = dragState.phantomIndex;
 
@@ -234,15 +241,18 @@ class ReorderFlexState extends State<ReorderFlex>
         }
 
         /// Determine the size of the drop area to show under the dragging widget.
-        final feedbackSize = dragState.feedbackSize;
+        Size? feedbackSize = Size.zero;
+        if (widget.config.useMoveAnimation) {
+          feedbackSize = dragState.feedbackSize;
+        }
+
         Widget appearSpace = _makeAppearSpace(dragSpace, feedbackSize);
         Widget disappearSpace = _makeDisappearSpace(dragSpace, feedbackSize);
 
         /// When start dragging, the dragTarget, [ReorderDragTarget], will
         /// return a [IgnorePointerWidget] which size is zero.
         if (dragState.isPhantomAboveDragTarget()) {
-          //the phantom is moving down, i.e. the tile below the phantom is moving up
-          Log.trace('index:$childIndex item moving up / phantom moving down');
+          _notifier.updateDragTargetIndex(currentIndex);
           if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
             return _buildDraggingContainer(children: [
               disappearSpace,
@@ -264,8 +274,7 @@ class ReorderFlexState extends State<ReorderFlex>
 
         ///
         if (dragState.isPhantomBelowDragTarget()) {
-          //the phantom is moving up, i.e. the tile above the phantom is moving down
-          Log.trace('index:$childIndex item moving down / phantom moving up');
+          _notifier.updateDragTargetIndex(currentIndex);
           if (shiftedIndex == currentIndex && childIndex == dragPhantomIndex) {
             return _buildDraggingContainer(children: [
               appearSpace,
@@ -303,10 +312,7 @@ class ReorderFlexState extends State<ReorderFlex>
   }
 
   ReorderDragTarget _buildDragTarget(
-    BuildContext builderContext,
-    Widget child,
-    int dragTargetIndex,
-  ) {
+      BuildContext builderContext, Widget child, int dragTargetIndex) {
     final ReoderFlexItem reorderFlexItem =
         widget.dataSource.items[dragTargetIndex];
     return ReorderDragTarget<FlexDragTargetData>(
@@ -319,14 +325,14 @@ class ReorderFlexState extends State<ReorderFlex>
       ),
       onDragStarted: (draggingWidget, draggingIndex, size) {
         Log.debug(
-            "[DragTarget] Column${widget.dataSource.identifier} start dragging item at $draggingIndex");
+            "[DragTarget] Column:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
         _startDragging(draggingWidget, draggingIndex, size);
         widget.onDragStarted?.call(draggingIndex);
       },
       onDragEnded: (dragTargetData) {
         Log.debug(
-            "[DragTarget]: Column${widget.dataSource.identifier} end dragging");
-
+            "[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging");
+        _notifier.updateDragTargetIndex(-1);
         setState(() {
           if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
             _onReordered(
@@ -340,14 +346,11 @@ class ReorderFlexState extends State<ReorderFlex>
         });
       },
       onWillAccept: (FlexDragTargetData dragTargetData) {
-        Log.debug('Insert animation: ${_animation.deleteController.status}');
-
         if (_animation.deleteController.isAnimating) {
           return false;
         }
 
         assert(widget.dataSource.items.length > dragTargetIndex);
-
         if (_interceptDragTarget(
           dragTargetData,
           (interceptor) => interceptor.onWillAccept(
@@ -370,6 +373,7 @@ class ReorderFlexState extends State<ReorderFlex>
         );
       },
       onLeave: (dragTargetData) {
+        _notifier.updateDragTargetIndex(-1);
         _interceptDragTarget(
           dragTargetData,
           (interceptor) => interceptor.onLeave(dragTargetData),
@@ -378,6 +382,7 @@ class ReorderFlexState extends State<ReorderFlex>
       insertAnimationController: _animation.insertController,
       deleteAnimationController: _animation.deleteController,
       draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
+      useMoveAnimation: widget.config.useMoveAnimation,
       child: child,
     );
   }
@@ -430,7 +435,7 @@ class ReorderFlexState extends State<ReorderFlex>
     /// The [willAccept] will be true if the dargTarget is the widget that gets
     /// dragged and it is dragged on top of the other dragTargets.
     ///
-    Log.trace(
+    Log.debug(
         '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}');
 
     bool willAccept =
@@ -442,7 +447,6 @@ class ReorderFlexState extends State<ReorderFlex>
       } else {
         dragState.updateNextIndex(dragTargetIndex);
       }
-
       _requestAnimationToNextIndex(isAcceptingNewTarget: true);
     });
 
@@ -467,7 +471,6 @@ class ReorderFlexState extends State<ReorderFlex>
     } else {
       return SingleChildScrollView(
         scrollDirection: widget.direction,
-        padding: widget.padding,
         controller: _scrollController,
         child: child,
       );

+ 54 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/widgets.dart';
 
 import '../transitions.dart';
+import 'drag_target.dart';
 
 mixin ReorderFlexMinxi {
   @protected
@@ -86,3 +87,56 @@ extension CurveAnimationController on AnimationController {
     );
   }
 }
+
+class ReorderFlexNotifier extends DragTargetMovePlaceholderDelegate {
+  Map<int, DragTargetEventNotifier> dragTargeEventNotifier = {};
+
+  void updateDragTargetIndex(int index) {
+    for (var notifier in dragTargeEventNotifier.values) {
+      notifier.setDragTargetIndex(index);
+    }
+  }
+
+  DragTargetEventNotifier _notifierFromIndex(int dragTargetIndex) {
+    DragTargetEventNotifier? notifier = dragTargeEventNotifier[dragTargetIndex];
+    if (notifier == null) {
+      final newNotifier = DragTargetEventNotifier();
+      dragTargeEventNotifier[dragTargetIndex] = newNotifier;
+      notifier = newNotifier;
+    }
+
+    return notifier;
+  }
+
+  void dispose() {
+    for (var notifier in dragTargeEventNotifier.values) {
+      notifier.dispose();
+    }
+  }
+
+  @override
+  void registerPlaceholder(
+    int dragTargetIndex,
+    void Function(int dragTargetIndex) callback,
+  ) {
+    _notifierFromIndex(dragTargetIndex).addListener(() {
+      callback.call(_notifierFromIndex(dragTargetIndex).currentDragTargetIndex);
+    });
+  }
+
+  @override
+  void unregisterPlaceholder(int dragTargetIndex) {
+    dragTargeEventNotifier.remove(dragTargetIndex);
+  }
+}
+
+class DragTargetEventNotifier extends ChangeNotifier {
+  int currentDragTargetIndex = -1;
+
+  void setDragTargetIndex(int index) {
+    if (currentDragTargetIndex != index) {
+      currentDragTargetIndex = index;
+      notifyListeners();
+    }
+  }
+}

+ 18 - 10
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart

@@ -1,9 +1,11 @@
+import 'dart:async';
+
 import 'package:flutter/material.dart';
 import '../../utils/log.dart';
 import '../board_column/board_column_data.dart';
-import '../flex/drag_state.dart';
-import '../flex/drag_target.dart';
-import '../flex/drag_target_inteceptor.dart';
+import '../reorder_flex/drag_state.dart';
+import '../reorder_flex/drag_target.dart';
+import '../reorder_flex/drag_target_inteceptor.dart';
 import 'phantom_state.dart';
 
 abstract class BoardPhantomControllerDelegate {
@@ -127,8 +129,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate
     FlexDragTargetData dragTargetData,
     int dragTargetIndex,
   ) {
-    // Log.debug('[$BoardPhantomController] move Column${dragTargetData.reorderFlexId}:${dragTargetData.draggingIndex} '
-    //     'to Column$columnId:$index');
+    // Log.debug('[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} '
+    //     'to Column:[$columnId]:$index');
 
     phantomRecord = PhantomRecord(
       toColumnId: columnId,
@@ -177,7 +179,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
   }
 
   @override
-  void didReturnOriginalDragTarget() {
+  void cancel() {
     if (phantomRecord == null) {
       return;
     }
@@ -188,7 +190,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
   }
 
   @override
-  void didCrossOtherDragTarget(
+  void moveTo(
     String reorderFlexId,
     FlexDragTargetData dragTargetData,
     int dragTargetIndex,
@@ -199,6 +201,12 @@ class BoardPhantomController extends OverlapDragTargetDelegate
       dragTargetIndex,
     );
   }
+
+  @override
+  bool canMoveTo(String dragTargetId) {
+    // TODO: implement shouldReceive
+    return delegate.controller(dragTargetId)?.columnData.items.length == 0;
+  }
 }
 
 /// Use [PhantomRecord] to record where to remove the column item and where to
@@ -228,7 +236,7 @@ class PhantomRecord {
       return;
     }
     Log.debug(
-        '[$PhantomRecord] Update Column$fromColumnId remove position to $index');
+        '[$PhantomRecord] Update Column:[$fromColumnId] remove position to $index');
     fromColumnIndex = index;
   }
 
@@ -238,13 +246,13 @@ class PhantomRecord {
     }
 
     Log.debug(
-        '[$PhantomRecord] Column$toColumnId update position $toColumnIndex -> $index');
+        '[$PhantomRecord] Column:[$toColumnId] update position $toColumnIndex -> $index');
     toColumnIndex = index;
   }
 
   @override
   String toString() {
-    return 'Column$fromColumnId:$fromColumnIndex to Column$toColumnId:$toColumnIndex';
+    return 'Column:[$fromColumnId]:$fromColumnIndex to Column:[$toColumnId]:$toColumnIndex';
   }
 }
 

+ 3 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/appflowy_styled_widgets.dart

@@ -0,0 +1,3 @@
+export 'card.dart';
+export 'footer.dart';
+export 'header.dart';

+ 37 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart

@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+
+class AppFlowyColumnItemCard extends StatefulWidget {
+  final Widget? child;
+  final Color backgroundColor;
+  final double cornerRadius;
+  final BoxConstraints boxConstraints;
+
+  const AppFlowyColumnItemCard({
+    this.child,
+    this.backgroundColor = Colors.white,
+    this.cornerRadius = 0.0,
+    this.boxConstraints = const BoxConstraints.tightFor(height: 60),
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<AppFlowyColumnItemCard> createState() => _AppFlowyColumnItemCardState();
+}
+
+class _AppFlowyColumnItemCardState extends State<AppFlowyColumnItemCard> {
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(4.0),
+      child: Container(
+        constraints: widget.boxConstraints,
+        clipBehavior: Clip.hardEdge,
+        decoration: BoxDecoration(
+          color: widget.backgroundColor,
+          borderRadius: BorderRadius.circular(widget.cornerRadius),
+        ),
+        child: widget.child,
+      ),
+    );
+  }
+}

+ 46 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart

@@ -0,0 +1,46 @@
+import 'package:flutter/material.dart';
+
+typedef OnFooterAddButtonClick = void Function();
+
+class AppFlowyColumnFooter extends StatefulWidget {
+  final double height;
+  final Widget? icon;
+  final Widget? title;
+  final EdgeInsets margin;
+  final OnFooterAddButtonClick? onAddButtonClick;
+
+  const AppFlowyColumnFooter({
+    this.icon,
+    this.title,
+    this.margin = EdgeInsets.zero,
+    required this.height,
+    this.onAddButtonClick,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<AppFlowyColumnFooter> createState() => _AppFlowyColumnFooterState();
+}
+
+class _AppFlowyColumnFooterState extends State<AppFlowyColumnFooter> {
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: widget.onAddButtonClick,
+      child: SizedBox(
+        height: widget.height,
+        child: Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 10),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              if (widget.icon != null) widget.icon!,
+              if (widget.title != null) widget.title!,
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 74 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart

@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+
+typedef OnHeaderAddButtonClick = void Function();
+typedef OnHeaderMoreButtonClick = void Function();
+
+class AppFlowyColumnHeader extends StatefulWidget {
+  final double height;
+  final Widget? icon;
+  final Widget? title;
+  final Widget? addIcon;
+  final Widget? moreIcon;
+  final EdgeInsets margin;
+  final OnHeaderAddButtonClick? onAddButtonClick;
+  final OnHeaderMoreButtonClick? onMoreButtonClick;
+
+  const AppFlowyColumnHeader({
+    required this.height,
+    this.icon,
+    this.title,
+    this.addIcon,
+    this.moreIcon,
+    this.margin = EdgeInsets.zero,
+    this.onAddButtonClick,
+    this.onMoreButtonClick,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<AppFlowyColumnHeader> createState() => _AppFlowyColumnHeaderState();
+}
+
+class _AppFlowyColumnHeaderState extends State<AppFlowyColumnHeader> {
+  @override
+  Widget build(BuildContext context) {
+    List<Widget> children = [];
+
+    if (widget.icon != null) {
+      children.add(widget.icon!);
+      children.add(_hSpace());
+    }
+
+    if (widget.title != null) {
+      children.add(widget.title!);
+      children.add(_hSpace());
+    }
+
+    if (widget.moreIcon != null) {
+      children.add(const Spacer());
+      children.add(
+        IconButton(onPressed: widget.onMoreButtonClick, icon: widget.moreIcon!),
+      );
+    }
+
+    if (widget.addIcon != null) {
+      children.add(
+        IconButton(onPressed: widget.onAddButtonClick, icon: widget.addIcon!),
+      );
+    }
+
+    return SizedBox(
+      height: widget.height,
+      child: Padding(
+        padding: widget.margin,
+        child: Row(
+          children: children,
+        ),
+      ),
+    );
+  }
+
+  Widget _hSpace() {
+    return const SizedBox(width: 6);
+  }
+}