Просмотр исходного кода

Merge pull request #472 from AppFlowy-IO/feat_row_expand

Feat: auto expand row height
Nathan.fooo 3 лет назад
Родитель
Сommit
2848ecb5ba
16 измененных файлов с 410 добавлено и 307 удалено
  1. 7 0
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service.dart
  2. 30 5
      frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart
  3. 1 1
      frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart
  4. 3 3
      frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart
  5. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart
  6. 155 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart
  7. 0 115
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart
  8. 1 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart
  9. 24 20
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart
  10. 0 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart
  11. 19 9
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart
  12. 58 50
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart
  13. 40 31
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart
  14. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/text_field.dart
  15. 46 45
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart
  16. 24 24
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart

+ 7 - 0
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service.dart

@@ -365,4 +365,11 @@ class GridCell with _$GridCell {
     required Field field,
     Cell? cell,
   }) = _GridCell;
+
+  // ignore: unused_element
+  const GridCell._();
+
+  String cellId() {
+    return rowId + field.id + "${field.fieldType}";
+  }
 }

+ 30 - 5
frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart

@@ -1,5 +1,7 @@
 import 'dart:collection';
 import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -28,7 +30,13 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             _rowService.createRow();
           },
           didReceiveCellDatas: (_DidReceiveCellDatas value) async {
-            emit(state.copyWith(cellDataMap: value.cellData));
+            final fields = value.gridCellMap.values.map((e) => CellSnapshot(e.field)).toList();
+            final snapshots = UnmodifiableListView(fields);
+            emit(state.copyWith(
+              gridCellMap: value.gridCellMap,
+              snapshots: snapshots,
+              changeReason: value.reason,
+            ));
           },
         );
       },
@@ -47,7 +55,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
   Future<void> _startListening() async {
     _rowListenFn = _rowCache.addRowListener(
       rowId: state.rowData.rowId,
-      onUpdated: (cellDatas) => add(RowEvent.didReceiveCellDatas(cellDatas)),
+      onUpdated: (cellDatas, reason) => add(RowEvent.didReceiveCellDatas(cellDatas, reason)),
       listenWhen: () => !isClosed,
     );
   }
@@ -57,18 +65,35 @@ class RowBloc extends Bloc<RowEvent, RowState> {
 class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
-  const factory RowEvent.didReceiveCellDatas(GridCellMap cellData) = _DidReceiveCellDatas;
+  const factory RowEvent.didReceiveCellDatas(GridCellMap gridCellMap, GridRowChangeReason reason) =
+      _DidReceiveCellDatas;
 }
 
 @freezed
 class RowState with _$RowState {
   const factory RowState({
     required GridRow rowData,
-    required GridCellMap cellDataMap,
+    required GridCellMap gridCellMap,
+    required UnmodifiableListView<CellSnapshot> snapshots,
+    GridRowChangeReason? changeReason,
   }) = _RowState;
 
   factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
         rowData: rowData,
-        cellDataMap: cellDataMap,
+        gridCellMap: cellDataMap,
+        snapshots: UnmodifiableListView(cellDataMap.values.map((e) => CellSnapshot(e.field)).toList()),
       );
 }
+
+class CellSnapshot extends Equatable {
+  final Field _field;
+
+  const CellSnapshot(Field field) : _field = field;
+
+  @override
+  List<Object?> get props => [
+        _field.id,
+        _field.fieldType,
+        _field.visibility,
+      ];
+}

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart

@@ -42,7 +42,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
   Future<void> _startListening() async {
     _rowListenFn = _rowCache.addRowListener(
       rowId: rowData.rowId,
-      onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
+      onUpdated: (cellDatas, reason) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
       listenWhen: () => !isClosed,
     );
   }

+ 3 - 3
frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

@@ -83,7 +83,7 @@ class GridRowCache {
 
   RowUpdateCallback addRowListener({
     required String rowId,
-    void Function(GridCellMap)? onUpdated,
+    void Function(GridCellMap, GridRowChangeReason)? onUpdated,
     bool Function()? listenWhen,
   }) {
     listenrHandler() async {
@@ -99,7 +99,7 @@ class GridRowCache {
         final row = _rowsNotifier.rowDataWithId(rowId);
         if (row != null) {
           final GridCellMap cellDataMap = _makeGridCells(rowId, row);
-          onUpdated(cellDataMap);
+          onUpdated(cellDataMap, _rowsNotifier._changeReason);
         }
       }
 
@@ -339,7 +339,7 @@ class GridRow with _$GridRow {
   const factory GridRow({
     required String gridId,
     required String rowId,
-    required List<Field> fields,
+    required UnmodifiableListView<Field> fields,
     required double height,
     Row? data,
   }) = _GridRow;

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart

@@ -9,7 +9,7 @@ class GridSize {
   static double get leadingHeaderPadding => 50 * scale;
   static double get trailHeaderPadding => 140 * scale;
   static double get headerContainerPadding => 0 * scale;
-  static double get cellHPadding => 10 * scale;
+  static double get cellHPadding => 8 * scale;
   static double get cellVPadding => 8 * scale;
   static double get typeOptionItemHeight => 32 * scale;
   static double get typeOptionSeparatorHeight => 6 * scale;

+ 155 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

@@ -2,6 +2,12 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
 import 'package:flutter/widgets.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+import 'package:styled_widget/styled_widget.dart';
 import 'checkbox_cell.dart';
 import 'date_cell.dart';
 import 'number_cell.dart';
@@ -9,7 +15,7 @@ import 'selection_cell/selection_cell.dart';
 import 'text_cell.dart';
 
 GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) {
-  final key = ValueKey(gridCell.rowId + gridCell.field.id);
+  final key = ValueKey(gridCell.cellId());
 
   final cellContextBuilder = GridCellContextBuilder(gridCell: gridCell, cellCache: cellCache);
 
@@ -51,9 +57,157 @@ abstract class GridCellWidget extends HoverWidget {
 }
 
 class GridCellRequestFocusNotifier extends ChangeNotifier {
+  VoidCallback? _listener;
+
+  @override
+  void addListener(VoidCallback listener) {
+    if (_listener != null) {
+      removeListener(_listener!);
+    }
+
+    _listener = listener;
+    super.addListener(listener);
+  }
+
+  void removeAllListener() {
+    if (_listener != null) {
+      removeListener(_listener!);
+    }
+  }
+
   void notify() {
     notifyListeners();
   }
 }
 
 abstract class GridCellStyle {}
+
+class CellSingleFocusNode extends FocusNode {
+  VoidCallback? _listener;
+
+  void setSingleListener(VoidCallback listener) {
+    if (_listener != null) {
+      removeListener(_listener!);
+    }
+
+    _listener = listener;
+    super.addListener(listener);
+  }
+
+  void removeSingleListener() {
+    if (_listener != null) {
+      removeListener(_listener!);
+    }
+  }
+}
+
+class CellStateNotifier extends ChangeNotifier {
+  bool _isFocus = false;
+  bool _onEnter = false;
+
+  set isFocus(bool value) {
+    if (_isFocus != value) {
+      _isFocus = value;
+      notifyListeners();
+    }
+  }
+
+  set onEnter(bool value) {
+    if (_onEnter != value) {
+      _onEnter = value;
+      notifyListeners();
+    }
+  }
+
+  bool get isFocus => _isFocus;
+
+  bool get onEnter => _onEnter;
+}
+
+class CellContainer extends StatelessWidget {
+  final GridCellWidget child;
+  final Widget? expander;
+  final double width;
+  final RegionStateNotifier rowStateNotifier;
+  const CellContainer({
+    Key? key,
+    required this.child,
+    required this.width,
+    required this.rowStateNotifier,
+    this.expander,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
+      create: (_) => CellStateNotifier(),
+      update: (_, row, cell) => cell!..onEnter = row.onEnter,
+      child: Selector<CellStateNotifier, bool>(
+        selector: (context, notifier) => notifier.isFocus,
+        builder: (context, isFocus, _) {
+          Widget container = Center(child: child);
+          child.onFocus.addListener(() {
+            Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
+          });
+
+          if (expander != null) {
+            container = _CellEnterRegion(child: container, expander: expander!);
+          }
+
+          return GestureDetector(
+            behavior: HitTestBehavior.translucent,
+            onTap: () => child.requestFocus.notify(),
+            child: Container(
+              constraints: BoxConstraints(maxWidth: width, minHeight: 46),
+              decoration: _makeBoxDecoration(context, isFocus),
+              padding: GridSize.cellContentInsets,
+              child: container,
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
+    final theme = context.watch<AppTheme>();
+    if (isFocus) {
+      final borderSide = BorderSide(color: theme.main1, width: 1.0);
+      return BoxDecoration(border: Border.fromBorderSide(borderSide));
+    } else {
+      final borderSide = BorderSide(color: theme.shader5, width: 1.0);
+      return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
+    }
+  }
+}
+
+class _CellEnterRegion extends StatelessWidget {
+  final Widget child;
+  final Widget expander;
+  const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Selector<CellStateNotifier, bool>(
+      selector: (context, notifier) => notifier.onEnter,
+      builder: (context, onEnter, _) {
+        List<Widget> children = [child];
+        if (onEnter) {
+          children.add(expander.positioned(right: 0));
+        }
+
+        return MouseRegion(
+          cursor: SystemMouseCursors.click,
+          onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
+          onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
+          child: Stack(
+            alignment: AlignmentDirectional.center,
+            fit: StackFit.expand,
+            // alignment: AlignmentDirectional.centerEnd,
+            children: children,
+          ),
+        );
+      },
+    );
+  }
+}

+ 0 - 115
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart

@@ -1,115 +0,0 @@
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
-import 'package:flowy_infra/theme.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
-import 'cell_builder.dart';
-
-class CellStateNotifier extends ChangeNotifier {
-  bool _isFocus = false;
-  bool _onEnter = false;
-
-  set isFocus(bool value) {
-    if (_isFocus != value) {
-      _isFocus = value;
-      notifyListeners();
-    }
-  }
-
-  set onEnter(bool value) {
-    if (_onEnter != value) {
-      _onEnter = value;
-      notifyListeners();
-    }
-  }
-
-  bool get isFocus => _isFocus;
-
-  bool get onEnter => _onEnter;
-}
-
-class CellContainer extends StatelessWidget {
-  final GridCellWidget child;
-  final Widget? expander;
-  final double width;
-  final RegionStateNotifier rowStateNotifier;
-  const CellContainer({
-    Key? key,
-    required this.child,
-    required this.width,
-    required this.rowStateNotifier,
-    this.expander,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
-      create: (_) => CellStateNotifier(),
-      update: (_, row, cell) => cell!..onEnter = row.onEnter,
-      child: Selector<CellStateNotifier, bool>(
-        selector: (context, notifier) => notifier.isFocus,
-        builder: (context, isFocus, _) {
-          Widget container = Center(child: child);
-          child.onFocus.addListener(() {
-            Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
-          });
-
-          if (expander != null) {
-            container = _CellEnterRegion(child: container, expander: expander!);
-          }
-
-          return GestureDetector(
-            behavior: HitTestBehavior.translucent,
-            onTap: () => child.requestFocus.notify(),
-            child: Container(
-              constraints: BoxConstraints(maxWidth: width),
-              decoration: _makeBoxDecoration(context, isFocus),
-              padding: GridSize.cellContentInsets,
-              child: container,
-            ),
-          );
-        },
-      ),
-    );
-  }
-
-  BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
-    final theme = context.watch<AppTheme>();
-    if (isFocus) {
-      final borderSide = BorderSide(color: theme.main1, width: 1.0);
-      return BoxDecoration(border: Border.fromBorderSide(borderSide));
-    } else {
-      final borderSide = BorderSide(color: theme.shader4, width: 0.4);
-      return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
-    }
-  }
-}
-
-class _CellEnterRegion extends StatelessWidget {
-  final Widget child;
-  final Widget expander;
-  const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Selector<CellStateNotifier, bool>(
-      selector: (context, notifier) => notifier.onEnter,
-      builder: (context, onEnter, _) {
-        List<Widget> children = [Expanded(child: child)];
-        if (onEnter) {
-          children.add(expander);
-        }
-
-        return MouseRegion(
-          cursor: SystemMouseCursors.click,
-          onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
-          onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
-          child: Row(
-            // alignment: AlignmentDirectional.centerEnd,
-            children: children,
-          ),
-        );
-      },
-    );
-  }
-}

+ 1 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart

@@ -57,6 +57,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
 
   @override
   Future<void> dispose() async {
+    widget.requestFocus.removeAllListener();
     _cellBloc.close();
     super.dispose();
   }

+ 24 - 20
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart

@@ -22,8 +22,7 @@ class NumberCell extends GridCellWidget {
 class _NumberCellState extends State<NumberCell> {
   late NumberCellBloc _cellBloc;
   late TextEditingController _controller;
-  late FocusNode _focusNode;
-  VoidCallback? _focusListener;
+  late CellSingleFocusNode _focusNode;
   Timer? _delayOperation;
 
   @override
@@ -31,11 +30,8 @@ class _NumberCellState extends State<NumberCell> {
     final cellContext = widget.cellContextBuilder.build();
     _cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode = FocusNode();
-    _focusNode.addListener(() {
-      widget.onFocus.value = _focusNode.hasFocus;
-      focusChanged();
-    });
+    _focusNode = CellSingleFocusNode();
+    _listenFocusNode();
     super.initState();
   }
 
@@ -55,7 +51,7 @@ class _NumberCellState extends State<NumberCell> {
             controller: _controller,
             focusNode: _focusNode,
             onEditingComplete: () => _focusNode.unfocus(),
-            maxLines: 1,
+            maxLines: null,
             style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
             decoration: const InputDecoration(
               contentPadding: EdgeInsets.zero,
@@ -70,15 +66,22 @@ class _NumberCellState extends State<NumberCell> {
 
   @override
   Future<void> dispose() async {
-    if (_focusListener != null) {
-      widget.requestFocus.removeListener(_focusListener!);
-    }
+    widget.requestFocus.removeAllListener();
     _delayOperation?.cancel();
     _cellBloc.close();
+    _focusNode.removeSingleListener();
     _focusNode.dispose();
     super.dispose();
   }
 
+  @override
+  void didUpdateWidget(covariant NumberCell oldWidget) {
+    if (oldWidget != widget) {
+      _listenFocusNode();
+    }
+    super.didUpdateWidget(oldWidget);
+  }
+
   Future<void> focusChanged() async {
     if (mounted) {
       _delayOperation?.cancel();
@@ -95,18 +98,19 @@ class _NumberCellState extends State<NumberCell> {
     }
   }
 
-  void _listenCellRequestFocus(BuildContext context) {
-    if (_focusListener != null) {
-      widget.requestFocus.removeListener(_focusListener!);
-    }
+  void _listenFocusNode() {
+    widget.onFocus.value = _focusNode.hasFocus;
+    _focusNode.setSingleListener(() {
+      widget.onFocus.value = _focusNode.hasFocus;
+      focusChanged();
+    });
+  }
 
-    focusListener() {
+  void _listenCellRequestFocus(BuildContext context) {
+    widget.requestFocus.addListener(() {
       if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
         FocusScope.of(context).requestFocus(_focusNode);
       }
-    }
-
-    _focusListener = focusListener;
-    widget.requestFocus.addListener(focusListener);
+    });
   }
 }

+ 0 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart

@@ -1,5 +1,4 @@
 export 'cell_builder.dart';
-export 'cell_container.dart';
 export 'text_cell.dart';
 export 'number_cell.dart';
 export 'date_cell.dart';

+ 19 - 9
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart

@@ -66,15 +66,25 @@ class SelectOptionTag extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Container(
-      decoration: BoxDecoration(
-        color: option.color.make(context),
-        shape: BoxShape.rectangle,
-        borderRadius: BorderRadius.circular(8.0),
-      ),
-      child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
-      margin: const EdgeInsets.symmetric(horizontal: 3.0),
-      padding: const EdgeInsets.symmetric(horizontal: 6.0),
+    return ChoiceChip(
+      pressElevation: 1,
+      label: FlowyText.medium(option.name, fontSize: 12),
+      selectedColor: option.color.make(context),
+      backgroundColor: option.color.make(context),
+      labelPadding: const EdgeInsets.symmetric(horizontal: 6),
+      selected: true,
+      onSelected: (_) {},
     );
+
+    // return Container(
+    //   decoration: BoxDecoration(
+    //     color: option.color.make(context),
+    //     shape: BoxShape.rectangle,
+    //     borderRadius: BorderRadius.circular(8.0),
+    //   ),
+    //   child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
+    //   margin: const EdgeInsets.symmetric(horizontal: 3.0),
+    //   padding: const EdgeInsets.symmetric(horizontal: 6.0),
+    // );
   }
 }

+ 58 - 50
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart

@@ -5,6 +5,7 @@ import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 // ignore: unused_import
 import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
@@ -44,51 +45,29 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
 
   @override
   void initState() {
-    // Log.trace("init widget $hashCode");
-    final cellContext = _buildCellContext();
+    final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
     _cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    // Log.trace("build widget $hashCode");
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
         builder: (context, state) {
-          List<Widget> children = [];
-          children.addAll(state.selectedOptions.map((option) => SelectOptionTag(option: option)).toList());
-
-          if (children.isEmpty && widget.cellStyle != null) {
-            children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3));
-          }
-          return SizedBox.expand(
-            child: InkWell(
-              onTap: () {
-                widget.onFocus.value = true;
-                SelectOptionCellEditor.show(
-                  context,
-                  _buildCellContext(),
-                  () => widget.onFocus.value = false,
-                );
-              },
-              child: ClipRRect(child: Row(children: children)),
-            ),
-          );
+          return _SelectOptionCell(
+              selectOptions: state.selectedOptions,
+              cellStyle: widget.cellStyle,
+              onFocus: (value) => widget.onFocus.value = value,
+              cellContextBuilder: widget.cellContextBuilder);
         },
       ),
     );
   }
 
-  GridSelectOptionCellContext _buildCellContext() {
-    return widget.cellContextBuilder.build() as GridSelectOptionCellContext;
-  }
-
   @override
   Future<void> dispose() async {
-    // Log.trace("dispose widget $hashCode");
     _cellBloc.close();
     super.dispose();
   }
@@ -120,7 +99,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
 
   @override
   void initState() {
-    final cellContext = _buildCellContext();
+    final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
     _cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
     super.initState();
   }
@@ -131,25 +110,11 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
       value: _cellBloc,
       child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
         builder: (context, state) {
-          List<Widget> children = state.selectedOptions.map((option) => SelectOptionTag(option: option)).toList();
-
-          if (children.isEmpty && widget.cellStyle != null) {
-            children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14));
-          }
-
-          return SizedBox.expand(
-            child: InkWell(
-              onTap: () {
-                widget.onFocus.value = true;
-                SelectOptionCellEditor.show(
-                  context,
-                  _buildCellContext(),
-                  () => widget.onFocus.value = false,
-                );
-              },
-              child: ClipRRect(child: Row(children: children)),
-            ),
-          );
+          return _SelectOptionCell(
+              selectOptions: state.selectedOptions,
+              cellStyle: widget.cellStyle,
+              onFocus: (value) => widget.onFocus.value = value,
+              cellContextBuilder: widget.cellContextBuilder);
         },
       ),
     );
@@ -160,8 +125,51 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
     _cellBloc.close();
     super.dispose();
   }
+}
+
+class _SelectOptionCell extends StatelessWidget {
+  final List<SelectOption> selectOptions;
+  final void Function(bool) onFocus;
+  final SelectOptionCellStyle? cellStyle;
+  final GridCellContextBuilder cellContextBuilder;
+  const _SelectOptionCell({
+    required this.selectOptions,
+    required this.onFocus,
+    required this.cellStyle,
+    required this.cellContextBuilder,
+    Key? key,
+  }) : super(key: key);
 
-  GridSelectOptionCellContext _buildCellContext() {
-    return widget.cellContextBuilder.build() as GridSelectOptionCellContext;
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    final Widget child;
+    if (selectOptions.isEmpty && cellStyle != null) {
+      child = Align(
+        alignment: Alignment.centerLeft,
+        child: FlowyText.medium(cellStyle!.placeholder, fontSize: 14, color: theme.shader3),
+      );
+    } else {
+      final tags = selectOptions.map((option) => SelectOptionTag(option: option)).toList();
+      child = Align(
+        alignment: Alignment.centerLeft,
+        child: Wrap(children: tags, spacing: 4, runSpacing: 4),
+      );
+    }
+
+    return Stack(
+      alignment: AlignmentDirectional.center,
+      fit: StackFit.expand,
+      children: [
+        child,
+        InkWell(
+          onTap: () {
+            onFocus(true);
+            final cellContext = cellContextBuilder.build() as GridSelectOptionCellContext;
+            SelectOptionCellEditor.show(context, cellContext, () => onFocus(false));
+          },
+        ),
+      ],
+    );
   }
 }

+ 40 - 31
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart

@@ -184,41 +184,50 @@ class _SelectOptionCell extends StatelessWidget {
     final theme = context.watch<AppTheme>();
     return SizedBox(
       height: GridSize.typeOptionItemHeight,
-      child: InkWell(
-        onTap: () {
-          context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
-        },
-        child: FlowyHover(
-          style: HoverStyle(hoverColor: theme.hover),
-          builder: (_, onHover) {
-            List<Widget> children = [
-              SelectOptionTag(option: option, isSelected: isSelected),
-              const Spacer(),
-            ];
-
-            if (isSelected) {
-              children.add(svgWidget("grid/checkmark"));
-            }
-
-            if (onHover) {
-              children.add(FlowyIconButton(
-                width: 30,
-                onPressed: () => _showEditPannel(context),
-                iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
-                icon: svgWidget("editor/details", color: theme.iconColor),
-              ));
-            }
-
-            return Padding(
-              padding: const EdgeInsets.all(3.0),
-              child: Row(children: children),
-            );
-          },
-        ),
+      child: Stack(
+        fit: StackFit.expand,
+        children: [
+          _body(theme, context),
+          InkWell(
+            onTap: () {
+              context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
+            },
+          ),
+        ],
       ),
     );
   }
 
+  FlowyHover _body(AppTheme theme, BuildContext context) {
+    return FlowyHover(
+      style: HoverStyle(hoverColor: theme.hover),
+      builder: (_, onHover) {
+        List<Widget> children = [
+          SelectOptionTag(option: option, isSelected: isSelected),
+          const Spacer(),
+        ];
+
+        if (isSelected) {
+          children.add(svgWidget("grid/checkmark"));
+        }
+
+        if (onHover) {
+          children.add(FlowyIconButton(
+            width: 30,
+            onPressed: () => _showEditPannel(context),
+            iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
+            icon: svgWidget("editor/details", color: theme.iconColor),
+          ));
+        }
+
+        return Padding(
+          padding: const EdgeInsets.all(3.0),
+          child: Row(children: children),
+        );
+      },
+    );
+  }
+
   void _showEditPannel(BuildContext context) {
     final pannel = EditSelectOptionPannel(
       option: option,

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/text_field.dart

@@ -94,7 +94,7 @@ class SelectOptionTextField extends StatelessWidget {
       child: SingleChildScrollView(
         controller: sc,
         scrollDirection: Axis.horizontal,
-        child: Row(children: children),
+        child: Wrap(children: children, spacing: 4),
       ),
     );
   }

+ 46 - 45
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart

@@ -35,8 +35,7 @@ class GridTextCell extends GridCellWidget {
 class _GridTextCellState extends State<GridTextCell> {
   late TextCellBloc _cellBloc;
   late TextEditingController _controller;
-  late FocusNode _focusNode;
-  VoidCallback? _focusListener;
+  late CellSingleFocusNode _focusNode;
   Timer? _delayOperation;
 
   @override
@@ -45,74 +44,76 @@ class _GridTextCellState extends State<GridTextCell> {
     _cellBloc = getIt<TextCellBloc>(param1: cellContext);
     _cellBloc.add(const TextCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode = FocusNode();
-    _focusNode.addListener(() {
-      widget.onFocus.value = _focusNode.hasFocus;
-      focusChanged();
-    });
+    _focusNode = CellSingleFocusNode();
 
+    _listenFocusNode();
+    _listenRequestFocus(context);
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
-    _listenCellRequestFocus(context);
-
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocConsumer<TextCellBloc, TextCellState>(
+      child: BlocListener<TextCellBloc, TextCellState>(
         listener: (context, state) {
           if (_controller.text != state.content) {
             _controller.text = state.content;
           }
         },
-        buildWhen: (previous, current) => previous.content != current.content,
-        builder: (context, state) {
-          return TextField(
-            controller: _controller,
-            focusNode: _focusNode,
-            onChanged: (value) => focusChanged(),
-            onEditingComplete: () => _focusNode.unfocus(),
-            maxLines: 1,
-            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-            decoration: InputDecoration(
-              contentPadding: EdgeInsets.zero,
-              border: InputBorder.none,
-              hintText: widget.cellStyle?.placeholder,
-              isDense: true,
-            ),
-          );
-        },
+        child: TextField(
+          controller: _controller,
+          focusNode: _focusNode,
+          onChanged: (value) => focusChanged(),
+          onEditingComplete: () => _focusNode.unfocus(),
+          maxLines: null,
+          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+          decoration: InputDecoration(
+            contentPadding: EdgeInsets.zero,
+            border: InputBorder.none,
+            hintText: widget.cellStyle?.placeholder,
+            isDense: true,
+          ),
+        ),
       ),
     );
   }
 
-  void _listenCellRequestFocus(BuildContext context) {
-    if (_focusListener != null) {
-      widget.requestFocus.removeListener(_focusListener!);
-    }
-
-    focusListener() {
-      if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
-        FocusScope.of(context).requestFocus(_focusNode);
-      }
-    }
-
-    _focusListener = focusListener;
-    widget.requestFocus.addListener(focusListener);
-  }
-
   @override
   Future<void> dispose() async {
-    if (_focusListener != null) {
-      widget.requestFocus.removeListener(_focusListener!);
-    }
+    widget.requestFocus.removeAllListener();
     _delayOperation?.cancel();
     _cellBloc.close();
+    _focusNode.removeSingleListener();
     _focusNode.dispose();
+
     super.dispose();
   }
 
+  @override
+  void didUpdateWidget(covariant GridTextCell oldWidget) {
+    if (oldWidget != widget) {
+      _listenFocusNode();
+    }
+    super.didUpdateWidget(oldWidget);
+  }
+
+  void _listenFocusNode() {
+    widget.onFocus.value = _focusNode.hasFocus;
+    _focusNode.setSingleListener(() {
+      widget.onFocus.value = _focusNode.hasFocus;
+      focusChanged();
+    });
+  }
+
+  void _listenRequestFocus(BuildContext context) {
+    widget.requestFocus.addListener(() {
+      if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
+        FocusScope.of(context).requestFocus(_focusNode);
+      }
+    });
+  }
+
   Future<void> focusChanged() async {
     if (mounted) {
       _delayOperation?.cancel();

+ 24 - 24
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart

@@ -4,6 +4,7 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/p
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
@@ -48,19 +49,13 @@ class _GridRowWidgetState extends State<GridRowWidget> {
         child: BlocBuilder<RowBloc, RowState>(
           buildWhen: (p, c) => p.rowData.height != c.rowData.height,
           builder: (context, state) {
-            final children = [
-              const _RowLeading(),
-              _RowCells(cellCache: widget.cellCache, onExpand: () => onExpandCell(context)),
-              const _RowTrailing(),
-            ];
-
-            final child = Row(
-              mainAxisSize: MainAxisSize.max,
-              crossAxisAlignment: CrossAxisAlignment.center,
-              children: children,
+            return Row(
+              children: [
+                const _RowLeading(),
+                Expanded(child: _RowCells(cellCache: widget.cellCache, onExpand: () => _expandRow(context))),
+                const _RowTrailing(),
+              ],
             );
-
-            return SizedBox(height: 42, child: child);
           },
         ),
       ),
@@ -73,7 +68,7 @@ class _GridRowWidgetState extends State<GridRowWidget> {
     super.dispose();
   }
 
-  void onExpandCell(BuildContext context) {
+  void _expandRow(BuildContext context) {
     final page = RowDetailPage(
       rowData: widget.rowData,
       rowCache: widget.rowCache,
@@ -159,13 +154,15 @@ class _RowCells extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocBuilder<RowBloc, RowState>(
-      buildWhen: (previous, current) => previous.cellDataMap.length != current.cellDataMap.length,
+      buildWhen: (previous, current) => !listEquals(previous.snapshots, current.snapshots),
       builder: (context, state) {
-        return Row(
-          mainAxisSize: MainAxisSize.min,
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: _makeCells(context, state.cellDataMap),
-        );
+        return IntrinsicHeight(
+            child: Row(
+          mainAxisSize: MainAxisSize.max,
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: _makeCells(context, state.gridCellMap),
+        ));
       },
     );
   }
@@ -209,11 +206,14 @@ class _CellExpander extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return FlowyIconButton(
-      width: 20,
-      onPressed: onExpand,
-      iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
-      icon: svgWidget("grid/expander", color: theme.main1),
+    return FittedBox(
+      fit: BoxFit.contain,
+      child: FlowyIconButton(
+        onPressed: onExpand,
+        iconPadding: const EdgeInsets.fromLTRB(6, 6, 6, 6),
+        fillColor: theme.surface,
+        icon: svgWidget("grid/expander", color: theme.main1),
+      ),
     );
   }
 }