Ver Fonte

Merge pull request #810 from LucasXu0/doc/flowy_selection_documentation

selection service documentation and implement auto wrap when selection changes.
Lucas.Xu há 2 anos atrás
pai
commit
eac54aefd7

+ 11 - 6
frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart

@@ -1,15 +1,20 @@
 library flowy_editor;
 
-export 'src/document/state_tree.dart';
 export 'src/document/node.dart';
 export 'src/document/path.dart';
+export 'src/document/position.dart';
+export 'src/document/selection.dart';
+export 'src/document/state_tree.dart';
 export 'src/document/text_delta.dart';
-export 'src/render/selection/selectable.dart';
+export 'src/editor_state.dart';
+export 'src/operation/operation.dart';
 export 'src/operation/transaction.dart';
 export 'src/operation/transaction_builder.dart';
-export 'src/operation/operation.dart';
-export 'src/editor_state.dart';
+export 'src/render/selection/selectable.dart';
 export 'src/service/editor_service.dart';
-export 'src/document/selection.dart';
-export 'src/document/position.dart';
 export 'src/service/render_plugin_service.dart';
+export 'src/service/service.dart';
+export 'src/service/selection_service.dart';
+export 'src/service/scroll_service.dart';
+export 'src/service/keyboard_service.dart';
+export 'src/service/input_service.dart';

+ 32 - 15
frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart

@@ -2,15 +2,26 @@ import 'package:flowy_editor/src/document/path.dart';
 import 'package:flowy_editor/src/document/position.dart';
 import 'package:flowy_editor/src/extensions/path_extensions.dart';
 
+/// Selection represents the selected area or the cursor area in the editor.
+///
+/// [Selection] is directional.
+///
+/// 1. forward,the end position is before the start position.
+/// 2. backward, the end position is after the start position.
+/// 3. collapsed, the end position is equal to the start position.
 class Selection {
-  final Position start;
-  final Position end;
-
+  /// Create a selection with [start], [end].
   Selection({
     required this.start,
     required this.end,
   });
 
+  /// Create a selection with [Path], [startOffset] and [endOffset].
+  ///
+  /// The [endOffset] is optional.
+  ///
+  /// This constructor will return a collapsed [Selection] if [endOffset] is null.
+  ///
   Selection.single({
     required Path path,
     required int startOffset,
@@ -18,10 +29,23 @@ class Selection {
   })  : start = Position(path: path, offset: startOffset),
         end = Position(path: path, offset: endOffset ?? startOffset);
 
+  /// Create a collapsed selection with [position].
   Selection.collapsed(Position position)
       : start = position,
         end = position;
 
+  final Position start;
+  final Position end;
+
+  bool get isCollapsed => start == end;
+  bool get isSingle => pathEquals(start.path, end.path);
+  bool get isForward =>
+      (start.path >= end.path && !pathEquals(start.path, end.path)) ||
+      (isSingle && start.offset > end.offset);
+  bool get isBackward =>
+      (start.path <= end.path && !pathEquals(start.path, end.path)) ||
+      (isSingle && start.offset < end.offset);
+
   Selection collapse({bool atStart = false}) {
     if (atStart) {
       return Selection(start: start, end: start);
@@ -30,13 +54,6 @@ class Selection {
     }
   }
 
-  bool get isCollapsed => start == end;
-  bool get isSingle => pathEquals(start.path, end.path);
-  bool get isUpward =>
-      start.path >= end.path && !pathEquals(start.path, end.path);
-  bool get isDownward =>
-      start.path <= end.path && !pathEquals(start.path, end.path);
-
   Selection copyWith({Position? start, Position? end}) {
     return Selection(
       start: start ?? this.start,
@@ -46,13 +63,10 @@ class Selection {
 
   Selection copy() => Selection(start: start, end: end);
 
-  @override
-  String toString() => '[Selection] start = $start, end = $end';
-
   Map<String, dynamic> toJson() {
     return {
-      "start": start.toJson(),
-      "end": end.toJson(),
+      'start': start.toJson(),
+      'end': end.toJson(),
     };
   }
 
@@ -69,4 +83,7 @@ class Selection {
 
   @override
   int get hashCode => Object.hash(start, end);
+
+  @override
+  String toString() => '[Selection] start = $start, end = $end';
 }

+ 1 - 1
frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart

@@ -59,7 +59,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
 
   @override
   Position end() => Position(
-      path: widget.textNode.path, offset: widget.textNode.toRawString().length);
+      path: widget.textNode.path, offset: widget.textNode.delta.length);
 
   @override
   Rect? getCursorRectInPosition(Position position) {

+ 2 - 22
frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart

@@ -1,3 +1,4 @@
+import 'package:flowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart';
 import 'package:flutter/material.dart';
 
 import 'package:flowy_editor/src/editor_state.dart';
@@ -9,15 +10,6 @@ import 'package:flowy_editor/src/render/rich_text/number_list_text.dart';
 import 'package:flowy_editor/src/render/rich_text/quoted_text.dart';
 import 'package:flowy_editor/src/render/rich_text/rich_text.dart';
 import 'package:flowy_editor/src/service/input_service.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
-import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
 import 'package:flowy_editor/src/service/keyboard_service.dart';
 import 'package:flowy_editor/src/service/render_plugin_service.dart';
 import 'package:flowy_editor/src/service/scroll_service.dart';
@@ -34,18 +26,6 @@ NodeWidgetBuilders defaultBuilders = {
   'text/quote': QuotedTextNodeWidgetBuilder(),
 };
 
-List<FlowyKeyEventHandler> defaultKeyEventHandler = [
-  deleteTextHandler,
-  slashShortcutHandler,
-  flowyDeleteNodesHandler,
-  arrowKeysHandler,
-  copyPasteKeysHandler,
-  redoUndoKeysHandler,
-  enterWithoutShiftInTextNodesHandler,
-  updateTextStyleByCommandXHandler,
-  whiteSpaceHandler,
-];
-
 class FlowyEditor extends StatefulWidget {
   const FlowyEditor({
     Key? key,
@@ -98,7 +78,7 @@ class _FlowyEditorState extends State<FlowyEditor> {
             child: FlowyKeyboard(
               key: editorState.service.keyboardServiceKey,
               handlers: [
-                ...defaultKeyEventHandler,
+                ...defaultKeyEventHandlers,
                 ...widget.keyEventHandlers,
               ],
               editorState: editorState,

+ 2 - 3
frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart

@@ -7,7 +7,7 @@ import 'package:flowy_editor/src/editor_state.dart';
 import 'package:flowy_editor/src/extensions/node_extensions.dart';
 import 'package:flowy_editor/src/operation/transaction_builder.dart';
 
-mixin FlowyInputService {
+abstract class FlowyInputService {
   void attach(TextEditingValue textEditingValue);
   void apply(List<TextEditingDelta> deltas);
   void close();
@@ -29,8 +29,7 @@ class FlowyInput extends StatefulWidget {
 }
 
 class _FlowyInputState extends State<FlowyInput>
-    with FlowyInputService
-    implements DeltaTextInputClient {
+    implements FlowyInputService, DeltaTextInputClient {
   TextInputConnection? _textInputConnection;
   TextRange? _composingTextRange;
 

+ 4 - 5
frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart

@@ -1,5 +1,4 @@
 import 'package:flowy_editor/flowy_editor.dart';
-import 'package:flowy_editor/src/service/keyboard_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
@@ -49,25 +48,25 @@ extension on Position {
 }
 
 Position? _goUp(EditorState editorState) {
-  final rects = editorState.service.selectionService.rects();
+  final rects = editorState.service.selectionService.selectionRects;
   if (rects.isEmpty) {
     return null;
   }
   final first = rects.first;
   final firstOffset = Offset(first.left, first.top);
   final hitOffset = firstOffset - Offset(0, first.height * 0.5);
-  return editorState.service.selectionService.hitTest(hitOffset);
+  return editorState.service.selectionService.getPositionInOffset(hitOffset);
 }
 
 Position? _goDown(EditorState editorState) {
-  final rects = editorState.service.selectionService.rects();
+  final rects = editorState.service.selectionService.selectionRects;
   if (rects.isEmpty) {
     return null;
   }
   final first = rects.last;
   final firstOffset = Offset(first.right, first.bottom);
   final hitOffset = firstOffset + Offset(0, first.height * 0.5);
-  return editorState.service.selectionService.hitTest(hitOffset);
+  return editorState.service.selectionService.getPositionInOffset(hitOffset);
 }
 
 KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {

+ 22 - 0
frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart

@@ -0,0 +1,22 @@
+import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
+import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
+import 'package:flowy_editor/src/service/keyboard_service.dart';
+
+List<FlowyKeyEventHandler> defaultKeyEventHandlers = [
+  deleteTextHandler,
+  slashShortcutHandler,
+  flowyDeleteNodesHandler,
+  arrowKeysHandler,
+  copyPasteKeysHandler,
+  redoUndoKeysHandler,
+  enterWithoutShiftInTextNodesHandler,
+  updateTextStyleByCommandXHandler,
+  whiteSpaceHandler,
+];

+ 1 - 1
frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart

@@ -67,7 +67,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler =
   // If selection is collapsed and position.start.offset == 0,
   //  insert a empty text node before.
   if (selection.isCollapsed && selection.start.offset == 0) {
-    if (textNode.toRawString().isEmpty) {
+    if (textNode.toRawString().isEmpty && textNode.subtype != null) {
       final afterSelection = Selection.collapsed(
         Position(path: textNode.path, offset: 0),
       );

+ 2 - 2
frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart

@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
 
 import 'package:flutter/material.dart';
 
-mixin FlowyKeyboardService<T extends StatefulWidget> on State<T> {
+abstract class FlowyKeyboardService {
   void enable();
   void disable();
 }
@@ -31,7 +31,7 @@ class FlowyKeyboard extends StatefulWidget {
 }
 
 class _FlowyKeyboardState extends State<FlowyKeyboard>
-    with FlowyKeyboardService {
+    implements FlowyKeyboardService {
   final FocusNode _focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
 
   bool isFocus = true;

+ 3 - 2
frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart

@@ -1,7 +1,7 @@
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 
-mixin FlowyScrollService<T extends StatefulWidget> on State<T> {
+abstract class FlowyScrollService {
   double get dy;
 
   void scrollTo(double dy);
@@ -22,7 +22,8 @@ class FlowyScroll extends StatefulWidget {
   State<FlowyScroll> createState() => _FlowyScrollState();
 }
 
-class _FlowyScrollState extends State<FlowyScroll> with FlowyScrollService {
+class _FlowyScrollState extends State<FlowyScroll>
+    implements FlowyScrollService {
   final _scrollController = ScrollController();
   final _scrollViewKey = GlobalKey();
 

+ 113 - 0
frontend/app_flowy/packages/flowy_editor/lib/src/service/selection/selection_gesture.dart

@@ -0,0 +1,113 @@
+import 'dart:async';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+
+/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
+/// for a while. So we need to implement our own GestureDetector.
+@immutable
+class SelectionGestureDetector extends StatefulWidget {
+  const SelectionGestureDetector({
+    Key? key,
+    this.child,
+    this.onTapDown,
+    this.onDoubleTapDown,
+    this.onTripleTapDown,
+    this.onPanStart,
+    this.onPanUpdate,
+    this.onPanEnd,
+  }) : super(key: key);
+
+  @override
+  State<SelectionGestureDetector> createState() =>
+      SelectionGestureDetectorState();
+
+  final Widget? child;
+
+  final GestureTapDownCallback? onTapDown;
+  final GestureTapDownCallback? onDoubleTapDown;
+  final GestureTapDownCallback? onTripleTapDown;
+  final GestureDragStartCallback? onPanStart;
+  final GestureDragUpdateCallback? onPanUpdate;
+  final GestureDragEndCallback? onPanEnd;
+}
+
+class SelectionGestureDetectorState extends State<SelectionGestureDetector> {
+  bool _isDoubleTap = false;
+  Timer? _doubleTapTimer;
+  int _tripleTabCount = 0;
+  Timer? _tripleTabTimer;
+
+  final kTripleTapTimeout = const Duration(milliseconds: 500);
+
+  @override
+  Widget build(BuildContext context) {
+    return RawGestureDetector(
+      behavior: HitTestBehavior.translucent,
+      gestures: {
+        PanGestureRecognizer:
+            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
+          () => PanGestureRecognizer(),
+          (recognizer) {
+            recognizer
+              ..onStart = widget.onPanStart
+              ..onUpdate = widget.onPanUpdate
+              ..onEnd = widget.onPanEnd;
+          },
+        ),
+        TapGestureRecognizer:
+            GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
+          () => TapGestureRecognizer(),
+          (recognizer) {
+            recognizer.onTapDown = _tapDownDelegate;
+          },
+        ),
+      },
+      child: widget.child,
+    );
+  }
+
+  _tapDownDelegate(TapDownDetails tapDownDetails) {
+    if (_tripleTabCount == 2) {
+      _tripleTabCount = 0;
+      _tripleTabTimer?.cancel();
+      _tripleTabTimer = null;
+      if (widget.onTripleTapDown != null) {
+        widget.onTripleTapDown!(tapDownDetails);
+      }
+    } else if (_isDoubleTap) {
+      _isDoubleTap = false;
+      _doubleTapTimer?.cancel();
+      _doubleTapTimer = null;
+      if (widget.onDoubleTapDown != null) {
+        widget.onDoubleTapDown!(tapDownDetails);
+      }
+      _tripleTabCount++;
+    } else {
+      if (widget.onTapDown != null) {
+        widget.onTapDown!(tapDownDetails);
+      }
+
+      _isDoubleTap = true;
+      _doubleTapTimer?.cancel();
+      _doubleTapTimer = Timer(kDoubleTapTimeout, () {
+        _isDoubleTap = false;
+        _doubleTapTimer = null;
+      });
+
+      _tripleTabCount = 1;
+      _tripleTabTimer?.cancel();
+      _tripleTabTimer = Timer(kTripleTapTimeout, () {
+        _tripleTabCount = 0;
+        _tripleTabTimer = null;
+      });
+    }
+  }
+
+  @override
+  void dispose() {
+    _doubleTapTimer?.cancel();
+    _tripleTabTimer?.cancel();
+    super.dispose();
+  }
+}

+ 284 - 443
frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart

@@ -1,81 +1,79 @@
-import 'dart:async';
-
 import 'package:flutter/foundation.dart';
-import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 
 import 'package:flowy_editor/src/document/node.dart';
 import 'package:flowy_editor/src/document/node_iterator.dart';
 import 'package:flowy_editor/src/document/position.dart';
 import 'package:flowy_editor/src/document/selection.dart';
-import 'package:flowy_editor/src/document/state_tree.dart';
 import 'package:flowy_editor/src/editor_state.dart';
 import 'package:flowy_editor/src/extensions/node_extensions.dart';
+import 'package:flowy_editor/src/extensions/object_extensions.dart';
+import 'package:flowy_editor/src/extensions/path_extensions.dart';
 import 'package:flowy_editor/src/render/selection/cursor_widget.dart';
 import 'package:flowy_editor/src/render/selection/selectable.dart';
 import 'package:flowy_editor/src/render/selection/selection_widget.dart';
-
-/// Process selection and cursor
-mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
-  /// Returns the current [Selection]
+import 'package:flowy_editor/src/service/selection/selection_gesture.dart';
+
+/// [FlowySelectionService] is responsible for processing
+/// the [Selection] changes and updates.
+///
+/// Usually, this service can be obtained by the following code.
+/// ```dart
+/// final selectionService = editorState.service.selectionService;
+///
+/// /** get current selection value*/
+/// final selection = selectionService.currentSelection.value;
+///
+/// /** get current selected nodes*/
+/// final nodes = selectionService.currentSelectedNodes;
+/// ```
+///
+abstract class FlowySelectionService {
+  /// The current [Selection] in editor.
+  ///
+  /// The value is null if there is no nodes are selected.
   ValueNotifier<Selection?> get currentSelection;
 
-  /// Returns the current selected [Node]s.
+  /// The current selected [Node]s in editor.
+  ///
+  /// The order of the result is determined according to the [currentSelection].
+  /// The result are ordered from back to front if the selection is forward.
+  /// The result are ordered from front to back if the selection is backward.
   ///
-  /// The order of the return is determined according to the selected order.
+  /// For example, Here is an array of selected nodes, [n1, n2, n3].
+  /// The result will be [n3, n2, n1] if the selection is forward,
+  ///   and [n1, n2, n3] if the selection is backward.
+  ///
+  /// Returns empty result if there is no nodes are selected.
   List<Node> get currentSelectedNodes;
 
-  /// Update the selection or cursor.
+  /// Updates the selection.
   ///
-  /// If selection is collapsed, this method will
-  ///   update the position of the cursor.
-  /// Otherwise, will update the selection.
+  /// The editor will update selection area and toolbar area
+  /// if the [selection] is not collapsed,
+  /// otherwise, will update the cursor area.
   void updateSelection(Selection selection);
 
-  /// Clear the selection or cursor.
+  /// Clears the selection area, cursor area and the popup list area.
   void clearSelection();
 
-  /// ------------------ Selection ------------------------
-
-  List<Rect> rects();
-
-  Position? hitTest(Offset? offset);
-
-  ///
+  /// Returns the [Node]s in [Selection].
   List<Node> getNodesInSelection(Selection selection);
 
-  /// ------------------ Selection ------------------------
-
-  /// ------------------ Offset ------------------------
-
-  /// Return the [Node] or [Null] in single selection.
+  /// Returns the [Node] containing to the [offset].
   ///
-  /// [offset] is under the global coordinate system.
+  /// [offset] must be under the global coordinate system.
   Node? getNodeInOffset(Offset offset);
 
-  /// Returns selected [Node]s. Empty list would be returned
-  ///   if no nodes are in range.
+  /// Returns the [Position] closest to the [offset].
   ///
+  /// Returns null if there is no nodes are selected.
   ///
-  /// [start] and [end] are under the global coordinate system.
-  ///
-  List<Node> getNodeInRange(Offset start, Offset end);
+  /// [offset] must be under the global coordinate system.
+  Position? getPositionInOffset(Offset offset);
 
-  /// Return [bool] to identify the [Node] is in Range or not.
-  ///
-  /// [start] and [end] are under the global coordinate system.
-  bool isNodeInRange(
-    Node node,
-    Offset start,
-    Offset end,
-  );
-
-  /// Return [bool] to identify the [Node] contains [Offset] or not.
-  ///
-  /// [offset] is under the global coordinate system.
-  bool isNodeInOffset(Node node, Offset offset);
-
-  /// ------------------ Offset ------------------------
+  /// The current selection areas's rect in editor.
+  List<Rect> get selectionRects;
 }
 
 class FlowySelection extends StatefulWidget {
@@ -97,41 +95,29 @@ class FlowySelection extends StatefulWidget {
 }
 
 class _FlowySelectionState extends State<FlowySelection>
-    with FlowySelectionService, WidgetsBindingObserver {
+    with WidgetsBindingObserver
+    implements FlowySelectionService {
   final _cursorKey = GlobalKey(debugLabel: 'cursor');
 
-  final List<OverlayEntry> _selectionOverlays = [];
-  final List<OverlayEntry> _cursorOverlays = [];
+  @override
+  final List<Rect> selectionRects = [];
+  final List<OverlayEntry> _selectionAreas = [];
+  final List<OverlayEntry> _cursorAreas = [];
+
   OverlayEntry? _debugOverlay;
 
-  /// [Pan] and [Tap] must be mutually exclusive.
   /// Pan
-  Offset? panStartOffset;
-  double? panStartScrollDy;
-  Offset? panEndOffset;
-
-  /// Tap
-  Offset? tapOffset;
-
-  final List<Rect> _rects = [];
+  Offset? _panStartOffset;
+  double? _panStartScrollDy;
 
   EditorState get editorState => widget.editorState;
 
-  @override
-  ValueNotifier<Selection?> currentSelection = ValueNotifier(null);
-
-  @override
-  List<Node> currentSelectedNodes = [];
-
-  @override
-  List<Node> getNodesInSelection(Selection selection) =>
-      _selectedNodesInSelection(editorState.document, selection);
-
   @override
   void initState() {
     super.initState();
 
     WidgetsBinding.instance.addObserver(this);
+    currentSelection.addListener(_onSelectionChange);
   }
 
   @override
@@ -148,13 +134,14 @@ class _FlowySelectionState extends State<FlowySelection>
   void dispose() {
     clearSelection();
     WidgetsBinding.instance.removeObserver(this);
+    currentSelection.removeListener(_onSelectionChange);
 
     super.dispose();
   }
 
   @override
   Widget build(BuildContext context) {
-    return _SelectionGestureDetector(
+    return SelectionGestureDetector(
       onPanStart: _onPanStart,
       onPanUpdate: _onPanUpdate,
       onPanEnd: _onPanEnd,
@@ -166,23 +153,48 @@ class _FlowySelectionState extends State<FlowySelection>
   }
 
   @override
-  List<Rect> rects() {
-    return _rects;
+  ValueNotifier<Selection?> currentSelection = ValueNotifier(null);
+
+  @override
+  List<Node> currentSelectedNodes = [];
+
+  @override
+  List<Node> getNodesInSelection(Selection selection) {
+    final start =
+        selection.isBackward ? selection.start.path : selection.end.path;
+    final end =
+        selection.isBackward ? selection.end.path : selection.start.path;
+    assert(start <= end);
+    final startNode = editorState.document.nodeAtPath(start);
+    final endNode = editorState.document.nodeAtPath(end);
+    if (startNode != null && endNode != null) {
+      final nodes =
+          NodeIterator(editorState.document, startNode, endNode).toList();
+      if (selection.isBackward) {
+        return nodes;
+      } else {
+        return nodes.reversed.toList(growable: false);
+      }
+    }
+    return [];
   }
 
   @override
   void updateSelection(Selection selection) {
-    _rects.clear();
+    selectionRects.clear();
     clearSelection();
 
-    // cursor
     if (selection.isCollapsed) {
-      debugPrint('Update cursor');
-      _updateCursor(selection.start);
+      /// updates cursor area.
+      debugPrint('updating cursor');
+      _updateCursorAreas(selection.start);
     } else {
-      debugPrint('Update selection');
-      _updateSelection(selection);
+      // updates selection area.
+      debugPrint('updating selection');
+      _updateSelectionAreas(selection);
     }
+
+    currentSelection.value = selection;
   }
 
   @override
@@ -190,224 +202,172 @@ class _FlowySelectionState extends State<FlowySelection>
     currentSelectedNodes = [];
     currentSelection.value = null;
 
-    // clear selection
-    _selectionOverlays
+    // clear selection areas
+    _selectionAreas
       ..forEach((overlay) => overlay.remove())
       ..clear();
-    // clear cursors
-    _cursorOverlays
+    // clear cursor areas
+    _cursorAreas
       ..forEach((overlay) => overlay.remove())
       ..clear();
-    // clear toolbar
+    // hide toolbar
     editorState.service.toolbarService?.hide();
   }
 
   @override
   Node? getNodeInOffset(Offset offset) {
-    return _lowerBoundInDocument(offset);
-  }
-
-  @override
-  List<Node> getNodeInRange(Offset start, Offset end) {
-    final startNode = _lowerBoundInDocument(start);
-    final endNode = _upperBoundInDocument(end);
-    return NodeIterator(editorState.document, startNode, endNode).toList();
-  }
-
-  @override
-  bool isNodeInOffset(Node node, Offset offset) {
-    final renderBox = node.renderBox;
-    if (renderBox != null) {
-      final boxOffset = renderBox.localToGlobal(Offset.zero);
-      final boxRect = boxOffset & renderBox.size;
-      return boxRect.contains(offset);
-    }
-    return false;
+    final sortedNodes =
+        editorState.document.root.children.toList(growable: false);
+    return _getNodeInOffset(
+      sortedNodes,
+      offset,
+      0,
+      sortedNodes.length - 1,
+    );
   }
 
   @override
-  bool isNodeInRange(Node node, Offset start, Offset end) {
-    final renderBox = node.renderBox;
-    if (renderBox != null) {
-      final rect = Rect.fromPoints(start, end);
-      final boxOffset = renderBox.localToGlobal(Offset.zero);
-      final boxRect = boxOffset & renderBox.size;
-      return rect.overlaps(boxRect);
-    }
-    return false;
-  }
-
-  void _onDoubleTapDown(TapDownDetails details) {
-    final offset = details.globalPosition;
+  Position? getPositionInOffset(Offset offset) {
     final node = getNodeInOffset(offset);
-    if (node == null) {
-      editorState.updateCursorSelection(null);
-      return;
-    }
-    final selectable = node.selectable;
+    final selectable = node?.selectable;
     if (selectable == null) {
-      editorState.updateCursorSelection(null);
-      return;
-    }
-    editorState
-        .updateCursorSelection(selectable.getWorldBoundaryInOffset(offset));
-  }
-
-  void _onTripleTapDown(TapDownDetails details) {
-    final offset = details.globalPosition;
-    final node = getNodeInOffset(offset);
-    if (node == null) {
-      editorState.updateCursorSelection(null);
-      return;
-    }
-    Selection selection;
-    if (node is TextNode) {
-      final textLen = node.delta.length;
-      selection = Selection(
-          start: Position(path: node.path, offset: 0),
-          end: Position(path: node.path, offset: textLen));
-    } else {
-      selection = Selection.collapsed(Position(path: node.path, offset: 0));
+      clearSelection();
+      return null;
     }
-    editorState.updateCursorSelection(selection);
+    return selectable.getPositionInOffset(offset);
   }
 
   void _onTapDown(TapDownDetails details) {
     // clear old state.
-    panStartOffset = null;
-    panEndOffset = null;
-
-    tapOffset = details.globalPosition;
+    _panStartOffset = null;
 
-    final position = hitTest(tapOffset);
+    final position = getPositionInOffset(details.globalPosition);
     if (position == null) {
       return;
     }
     final selection = Selection.collapsed(position);
-    editorState.updateCursorSelection(selection);
+    updateSelection(selection);
 
-    editorState.service.keyboardService?.enable();
-    editorState.service.scrollService?.enable();
+    _enableInteraction();
+
+    _showDebugLayerIfNeeded(offset: details.globalPosition);
   }
 
-  @override
-  Position? hitTest(Offset? offset) {
-    if (offset == null) {
-      editorState.updateCursorSelection(null);
-      return null;
-    }
+  void _onDoubleTapDown(TapDownDetails details) {
+    final offset = details.globalPosition;
     final node = getNodeInOffset(offset);
-    if (node == null) {
-      editorState.updateCursorSelection(null);
-      return null;
+    final selection = node?.selectable?.getWorldBoundaryInOffset(offset);
+    if (selection == null) {
+      clearSelection();
+      return;
     }
-    final selectable = node.selectable;
+    updateSelection(selection);
+
+    _enableInteraction();
+  }
+
+  void _onTripleTapDown(TapDownDetails details) {
+    final offset = details.globalPosition;
+    final node = getNodeInOffset(offset);
+    final selectable = node?.selectable;
     if (selectable == null) {
-      editorState.updateCursorSelection(null);
-      return null;
+      clearSelection();
+      return;
     }
-    return selectable.getPositionInOffset(offset);
+    Selection selection = Selection(
+      start: selectable.start(),
+      end: selectable.end(),
+    );
+    updateSelection(selection);
+
+    _enableInteraction();
   }
 
   void _onPanStart(DragStartDetails details) {
-    // clear old state.
-    panEndOffset = null;
-    tapOffset = null;
     clearSelection();
 
-    panStartOffset = details.globalPosition;
-    panStartScrollDy = editorState.service.scrollService?.dy;
+    _panStartOffset = details.globalPosition;
+    _panStartScrollDy = editorState.service.scrollService?.dy;
 
-    debugPrint('[_onPanStart] panStartOffset = $panStartOffset');
+    _enableInteraction();
   }
 
   void _onPanUpdate(DragUpdateDetails details) {
-    if (panStartOffset == null || panStartScrollDy == null) {
+    if (_panStartOffset == null || _panStartScrollDy == null) {
       return;
     }
 
-    editorState.service.keyboardService?.enable();
-    editorState.service.scrollService?.enable();
+    _enableInteraction();
 
-    panEndOffset = details.globalPosition;
+    final panEndOffset = details.globalPosition;
     final dy = editorState.service.scrollService?.dy;
-    var panStartOffsetWithScrollDyGap = panStartOffset!;
-    if (dy != null) {
-      panStartOffsetWithScrollDyGap =
-          panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
-    }
+    final panStartOffset = dy == null
+        ? _panStartOffset!
+        : _panStartOffset!.translate(0, _panStartScrollDy! - dy);
 
-    final first =
-        _lowerBoundInDocument(panStartOffsetWithScrollDyGap).selectable;
-    final last = _upperBoundInDocument(panEndOffset!).selectable;
+    final first = getNodeInOffset(panStartOffset)?.selectable;
+    final last = getNodeInOffset(panEndOffset)?.selectable;
 
     // compute the selection in range.
     if (first != null && last != null) {
-      bool isDownward;
-      if (first == last) {
-        isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx;
-      } else {
-        isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy;
-      }
-      final start = first
-          .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
-          .start;
-      final end = last
-          .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
-          .end;
-      final selection = Selection(
-          start: isDownward ? start : end, end: isDownward ? end : start);
+      bool isDownward = (identical(first, last))
+          ? panStartOffset.dx < panEndOffset.dx
+          : panStartOffset.dy < panEndOffset.dy;
+      final start =
+          first.getSelectionInRange(panStartOffset, panEndOffset).start;
+      final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
+      final selection = Selection(start: start, end: end);
       debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
-      editorState.updateCursorSelection(selection);
-
-      _scrollUpOrDownIfNeeded(panEndOffset!, isDownward);
+      updateSelection(selection);
     }
 
-    _showDebugLayerIfNeeded();
+    _showDebugLayerIfNeeded(offset: panEndOffset);
   }
 
   void _onPanEnd(DragEndDetails details) {
     // do nothing
   }
 
-  void _updateSelection(Selection selection) {
-    final nodes = _selectedNodesInSelection(editorState.document, selection);
+  void _updateSelectionAreas(Selection selection) {
+    final nodes = getNodesInSelection(selection);
 
     currentSelectedNodes = nodes;
-    currentSelection.value = selection;
 
+    // TODO: need to be refactored.
     Rect? topmostRect;
     LayerLink? layerLink;
 
-    var index = 0;
-    for (final node in nodes) {
+    final backwardNodes =
+        selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
+    final backwardSelection = selection.isBackward
+        ? selection
+        : selection.copyWith(start: selection.end, end: selection.start);
+    assert(backwardSelection.isBackward);
+
+    for (var i = 0; i < backwardNodes.length; i++) {
+      final node = backwardNodes[i];
       final selectable = node.selectable;
       if (selectable == null) {
         continue;
       }
 
-      var newSelection = selection.copy();
-      // In the case of multiple selections,
-      //  we need to return a new selection for each selected node individually.
-      if (!selection.isSingle) {
-        // <> means selected.
-        // text: abcd<ef
-        // text: ghijkl
-        // text: mn>opqr
-        if (index == 0) {
-          if (selection.isDownward) {
-            newSelection = selection.copyWith(end: selectable.end());
-          } else {
-            newSelection = selection.copyWith(start: selectable.start());
-          }
-        } else if (index == nodes.length - 1) {
-          if (selection.isDownward) {
-            newSelection = selection.copyWith(start: selectable.start());
-          } else {
-            newSelection = selection.copyWith(end: selectable.end());
-          }
+      var newSelection = backwardSelection.copy();
+
+      /// In the case of multiple selections,
+      ///  we need to return a new selection for each selected node individually.
+      ///
+      /// < > means selected.
+      /// text: abcd<ef
+      /// text: ghijkl
+      /// text: mn>opqr
+      ///
+      if (!backwardSelection.isSingle) {
+        if (i == 0) {
+          newSelection = newSelection.copyWith(end: selectable.end());
+        } else if (i == nodes.length - 1) {
+          newSelection = newSelection.copyWith(start: selectable.start());
         } else {
-          newSelection = selection.copyWith(
+          newSelection = Selection(
             start: selectable.start(),
             end: selectable.end(),
           );
@@ -415,13 +375,13 @@ class _FlowySelectionState extends State<FlowySelection>
       }
 
       final rects = selectable.getRectsInSelection(newSelection);
-
       for (final rect in rects) {
-        // FIXME: Need to compute more precise location.
+        // TODO: Need to compute more precise location.
         topmostRect ??= rect;
         layerLink ??= node.layerLink;
 
-        _rects.add(_transformRectToGlobal(selectable, rect));
+        selectionRects.add(_transformRectToGlobal(selectable, rect));
+
         final overlay = OverlayEntry(
           builder: (context) => SelectionWidget(
             color: widget.selectionColor,
@@ -429,11 +389,11 @@ class _FlowySelectionState extends State<FlowySelection>
             rect: rect,
           ),
         );
-        _selectionOverlays.add(overlay);
+        _selectionAreas.add(overlay);
       }
-      index += 1;
     }
-    Overlay.of(context)?.insertAll(_selectionOverlays);
+
+    Overlay.of(context)?.insertAll(_selectionAreas);
 
     if (topmostRect != null && layerLink != null) {
       editorState.service.toolbarService
@@ -441,123 +401,84 @@ class _FlowySelectionState extends State<FlowySelection>
     }
   }
 
-  Rect _transformRectToGlobal(Selectable selectable, Rect r) {
-    final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
-    return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
-  }
-
-  void _updateCursor(Position position) {
+  void _updateCursorAreas(Position position) {
     final node = editorState.document.root.childAtPath(position.path);
 
-    assert(node != null);
     if (node == null) {
+      assert(false);
       return;
     }
 
     currentSelectedNodes = [node];
-    currentSelection.value = Selection.collapsed(position);
 
+    _showCursor(node, position);
+  }
+
+  void _showCursor(Node node, Position position) {
     final selectable = node.selectable;
-    final rect = selectable?.getCursorRectInPosition(position);
-    if (rect != null) {
-      _rects.add(_transformRectToGlobal(selectable!, rect));
-      final cursor = OverlayEntry(
+    final cursorRect = selectable?.getCursorRectInPosition(position);
+    if (selectable != null && cursorRect != null) {
+      final cursorArea = OverlayEntry(
         builder: (context) => CursorWidget(
           key: _cursorKey,
-          rect: rect,
+          rect: cursorRect,
           color: widget.cursorColor,
           layerLink: node.layerLink,
         ),
       );
-      _cursorOverlays.add(cursor);
-      Overlay.of(context)?.insertAll(_cursorOverlays);
+
+      _cursorAreas.add(cursorArea);
+      selectionRects.add(_transformRectToGlobal(selectable, cursorRect));
+      Overlay.of(context)?.insertAll(_cursorAreas);
+
       _forceShowCursor();
     }
   }
 
-  _forceShowCursor() {
-    final currentState = _cursorKey.currentState as CursorWidgetState?;
-    currentState?.show();
-  }
-
-  List<Node> _selectedNodesInSelection(
-      StateTree stateTree, Selection selection) {
-    final startNode = stateTree.nodeAtPath(selection.start.path)!;
-    final endNode = stateTree.nodeAtPath(selection.end.path)!;
-    return NodeIterator(stateTree, startNode, endNode).toList();
+  void _forceShowCursor() {
+    _cursorKey.currentState?.unwrapOrNull<CursorWidgetState>()?.show();
   }
 
-  void _scrollUpOrDownIfNeeded(Offset offset, bool isDownward) {
+  void _scrollUpOrDownIfNeeded() {
     final dy = editorState.service.scrollService?.dy;
-    if (dy == null) {
-      assert(false, 'Dy could not be null');
+    final selectNodes = currentSelectedNodes;
+    final selection = currentSelection.value;
+    if (dy == null || selection == null || selectNodes.isEmpty) {
       return;
     }
-    final topLimit = MediaQuery.of(context).size.height * 0.2;
-    final bottomLimit = MediaQuery.of(context).size.height * 0.8;
+
+    final rect = selectNodes.last.rect;
+
+    final size = MediaQuery.of(context).size.height;
+    final topLimit = size * 0.3;
+    final bottomLimit = size * 0.8;
 
     /// TODO: It is necessary to calculate the relative speed
     ///   according to the gap and move forward more gently.
-    final distance = 10.0;
-    if (offset.dy <= topLimit && !isDownward) {
-      // up
-      editorState.service.scrollService?.scrollTo(dy - distance);
-    } else if (offset.dy >= bottomLimit && isDownward) {
-      //down
-      editorState.service.scrollService?.scrollTo(dy + distance);
-    }
-  }
-
-  void _showDebugLayerIfNeeded() {
-    // remove false to show debug overlay.
-    if (kDebugMode && false) {
-      _debugOverlay?.remove();
-      if (panStartOffset != null) {
-        _debugOverlay = OverlayEntry(
-          builder: (context) => Positioned.fromRect(
-            rect: Rect.fromPoints(
-                    panStartOffset?.translate(
-                          0,
-                          -(editorState.service.scrollService!.dy -
-                              panStartScrollDy!),
-                        ) ??
-                        Offset.zero,
-                    panEndOffset ?? Offset.zero)
-                .translate(0, 0),
-            child: Container(
-              color: Colors.red.withOpacity(0.2),
-            ),
-          ),
-        );
-        Overlay.of(context)?.insert(_debugOverlay!);
-      } else {
-        _debugOverlay = null;
+    if (rect.top >= bottomLimit) {
+      if (selection.isSingle) {
+        editorState.service.scrollService?.scrollTo(dy + size * 0.2);
+      } else if (selection.isBackward) {
+        editorState.service.scrollService?.scrollTo(dy + 10.0);
+      }
+    } else if (rect.bottom <= topLimit) {
+      if (selection.isForward) {
+        editorState.service.scrollService?.scrollTo(dy - 10.0);
       }
     }
   }
 
-  Node _lowerBoundInDocument(Offset offset) {
-    final sortedNodes =
-        editorState.document.root.children.toList(growable: false);
-    return _lowerBound(sortedNodes, offset, 0, sortedNodes.length - 1);
-  }
-
-  Node _upperBoundInDocument(Offset offset) {
-    final sortedNodes =
-        editorState.document.root.children.toList(growable: false);
-    return _upperBound(sortedNodes, offset, 0, sortedNodes.length - 1);
-  }
-
-  /// TODO: Supports multi-level nesting,
-  ///  currently only single-level nesting is supported
-  // find the first node's rect.bottom <= offset.dy
-  Node _lowerBound(List<Node> sortedNodes, Offset offset, int start, int end) {
-    assert(start >= 0 && end < sortedNodes.length);
+  Node? _getNodeInOffset(
+      List<Node> sortedNodes, Offset offset, int start, int end) {
+    if (start < 0 && end >= sortedNodes.length) {
+      return null;
+    }
     var min = start;
     var max = end;
     while (min <= max) {
       final mid = min + ((max - min) >> 1);
-      if (sortedNodes[mid].rect.bottom <= offset.dy) {
+      final rect = sortedNodes[mid].rect;
+      if (rect.bottom <= offset.dy) {
         min = mid + 1;
       } else {
         max = mid - 1;
@@ -566,144 +487,64 @@ class _FlowySelectionState extends State<FlowySelection>
     final node = sortedNodes[min];
     if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) {
       final children = node.children.toList(growable: false);
-      return _lowerBound(children, offset, 0, children.length - 1);
+      return _getNodeInOffset(
+        children,
+        offset,
+        0,
+        children.length - 1,
+      );
     }
     return node;
   }
 
-  /// TODO: Supports multi-level nesting,
-  ///  currently only single-level nesting is supported
-  // find the first node's rect.top < offset.dy
-  Node _upperBound(
-    List<Node> sortedNodes,
-    Offset offset,
-    int start,
-    int end,
-  ) {
-    assert(start >= 0 && end < sortedNodes.length);
-    var min = start;
-    var max = end;
-    while (min <= max) {
-      final mid = min + ((max - min) >> 1);
-      if (sortedNodes[mid].rect.top < offset.dy) {
-        min = mid + 1;
-      } else {
-        max = mid - 1;
-      }
-    }
-    final node = sortedNodes[max];
-    if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) {
-      final children = node.children.toList(growable: false);
-      return _lowerBound(children, offset, 0, children.length - 1);
-    }
-    return node;
+  void _enableInteraction() {
+    editorState.service.keyboardService?.enable();
+    editorState.service.scrollService?.enable();
   }
-}
-
-/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
-/// for a while. So we need to implement our own GestureDetector.
-@immutable
-class _SelectionGestureDetector extends StatefulWidget {
-  const _SelectionGestureDetector(
-      {Key? key,
-      this.child,
-      this.onTapDown,
-      this.onDoubleTapDown,
-      this.onTripleTapDown,
-      this.onPanStart,
-      this.onPanUpdate,
-      this.onPanEnd})
-      : super(key: key);
 
-  @override
-  State<_SelectionGestureDetector> createState() =>
-      _SelectionGestureDetectorState();
-
-  final Widget? child;
-
-  final GestureTapDownCallback? onTapDown;
-  final GestureTapDownCallback? onDoubleTapDown;
-  final GestureTapDownCallback? onTripleTapDown;
-  final GestureDragStartCallback? onPanStart;
-  final GestureDragUpdateCallback? onPanUpdate;
-  final GestureDragEndCallback? onPanEnd;
-}
-
-const Duration kTripleTapTimeout = Duration(milliseconds: 500);
+  Rect _transformRectToGlobal(Selectable selectable, Rect r) {
+    final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
+    return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
+  }
 
-class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
-  bool _isDoubleTap = false;
-  Timer? _doubleTapTimer;
-  int _tripleTabCount = 0;
-  Timer? _tripleTabTimer;
-  @override
-  Widget build(BuildContext context) {
-    return RawGestureDetector(
-      behavior: HitTestBehavior.translucent,
-      gestures: {
-        PanGestureRecognizer:
-            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
-          () => PanGestureRecognizer(),
-          (recognizer) {
-            recognizer
-              ..onStart = widget.onPanStart
-              ..onUpdate = widget.onPanUpdate
-              ..onEnd = widget.onPanEnd;
-          },
-        ),
-        TapGestureRecognizer:
-            GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
-          () => TapGestureRecognizer(),
-          (recognizer) {
-            recognizer.onTapDown = _tapDownDelegate;
-          },
-        ),
-      },
-      child: widget.child,
-    );
+  void _onSelectionChange() {
+    _scrollUpOrDownIfNeeded();
   }
 
-  _tapDownDelegate(TapDownDetails tapDownDetails) {
-    if (_tripleTabCount == 2) {
-      _tripleTabCount = 0;
-      _tripleTabTimer?.cancel();
-      _tripleTabTimer = null;
-      if (widget.onTripleTapDown != null) {
-        widget.onTripleTapDown!(tapDownDetails);
-      }
-    } else if (_isDoubleTap) {
-      _isDoubleTap = false;
-      _doubleTapTimer?.cancel();
-      _doubleTapTimer = null;
-      if (widget.onDoubleTapDown != null) {
-        widget.onDoubleTapDown!(tapDownDetails);
-      }
-      _tripleTabCount++;
-    } else {
-      if (widget.onTapDown != null) {
-        widget.onTapDown!(tapDownDetails);
+  void _showDebugLayerIfNeeded({Offset? offset}) {
+    // remove false to show debug overlay.
+    if (kDebugMode && false) {
+      _debugOverlay?.remove();
+      if (offset != null) {
+        _debugOverlay = OverlayEntry(
+          builder: (context) => Positioned.fromRect(
+            rect: Rect.fromPoints(offset, offset.translate(20, 20)),
+            child: Container(
+              color: Colors.red.withOpacity(0.2),
+            ),
+          ),
+        );
+        Overlay.of(context)?.insert(_debugOverlay!);
+      } else if (_panStartOffset != null) {
+        _debugOverlay = OverlayEntry(
+          builder: (context) => Positioned.fromRect(
+            rect: Rect.fromPoints(
+                _panStartOffset?.translate(
+                      0,
+                      -(editorState.service.scrollService!.dy -
+                          _panStartScrollDy!),
+                    ) ??
+                    Offset.zero,
+                offset ?? Offset.zero),
+            child: Container(
+              color: Colors.red.withOpacity(0.2),
+            ),
+          ),
+        );
+        Overlay.of(context)?.insert(_debugOverlay!);
+      } else {
+        _debugOverlay = null;
       }
-
-      _isDoubleTap = true;
-      _doubleTapTimer?.cancel();
-      _doubleTapTimer = Timer(kDoubleTapTimeout, () {
-        _isDoubleTap = false;
-        _doubleTapTimer = null;
-      });
-
-      _tripleTabCount = 1;
-      _tripleTabTimer?.cancel();
-      _tripleTabTimer = Timer(kTripleTapTimeout, () {
-        _tripleTabCount = 0;
-        _tripleTabTimer = null;
-      });
     }
   }
-
-  @override
-  void dispose() {
-    _doubleTapTimer?.cancel();
-    _tripleTabTimer?.cancel();
-    super.dispose();
-  }
 }

+ 1 - 2
frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart

@@ -1,10 +1,9 @@
-import 'package:flutter/material.dart';
-
 import 'package:flowy_editor/src/service/keyboard_service.dart';
 import 'package:flowy_editor/src/service/render_plugin_service.dart';
 import 'package:flowy_editor/src/service/scroll_service.dart';
 import 'package:flowy_editor/src/service/selection_service.dart';
 import 'package:flowy_editor/src/service/toolbar_service.dart';
+import 'package:flutter/material.dart';
 
 class FlowyService {
   // selection service

+ 3 - 2
frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/src/render/selection/toolbar_widget.dart';
 
-mixin FlowyToolbarService {
+abstract class FlowyToolbarService {
   /// Show the toolbar widget beside the offset.
   void showInOffset(Offset offset, LayerLink layerLink);
 
@@ -25,7 +25,8 @@ class FlowyToolbar extends StatefulWidget {
   State<FlowyToolbar> createState() => _FlowyToolbarState();
 }
 
-class _FlowyToolbarState extends State<FlowyToolbar> with FlowyToolbarService {
+class _FlowyToolbarState extends State<FlowyToolbar>
+    implements FlowyToolbarService {
   OverlayEntry? _toolbarOverlay;
 
   @override