Ver código fonte

feat: double tap on text

Vincent Chan 2 anos atrás
pai
commit
2a09f69bec

+ 10 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart

@@ -108,6 +108,16 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
     return Position(path: _textNode.path, offset: baseOffset);
     return Position(path: _textNode.path, offset: baseOffset);
   }
   }
 
 
+  @override
+  Selection? getWorldBoundaryInOffset(Offset offset) {
+    final localOffset = _renderParagraph.globalToLocal(offset);
+    final textPosition = _renderParagraph.getPositionForOffset(localOffset);
+    final textRange = _renderParagraph.getWordBoundary(textPosition);
+    final start = Position(path: _textNode.path, offset: textRange.start);
+    final end = Position(path: _textNode.path, offset: textRange.end);
+    return Selection(start: start, end: end);
+  }
+
   @override
   @override
   List<Rect> getRectsInSelection(Selection selection) {
   List<Rect> getRectsInSelection(Selection selection) {
     assert(pathEquals(selection.start.path, selection.end.path) &&
     assert(pathEquals(selection.start.path, selection.end.path) &&

+ 4 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart

@@ -21,6 +21,10 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
   ///
   ///
   /// The return result must be an offset of the local coordinate system.
   /// The return result must be an offset of the local coordinate system.
   Position getPositionInOffset(Offset start);
   Position getPositionInOffset(Offset start);
+  Selection? getWorldBoundaryInOffset(Offset start) {
+    return null;
+  }
+
   Rect getCursorRectInPosition(Position position);
   Rect getCursorRectInPosition(Position position);
 
 
   Offset localToGlobal(Offset offset);
   Offset localToGlobal(Offset offset);

+ 111 - 22
frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart

@@ -1,3 +1,5 @@
+import 'dart:async';
+
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/position.dart';
 import 'package:flowy_editor/document/position.dart';
 import 'package:flowy_editor/document/selection.dart';
 import 'package:flowy_editor/document/selection.dart';
@@ -6,10 +8,10 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart';
 import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
 import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
 import 'package:flowy_editor/extensions/object_extensions.dart';
 import 'package:flowy_editor/extensions/object_extensions.dart';
 import 'package:flowy_editor/extensions/node_extensions.dart';
 import 'package:flowy_editor/extensions/node_extensions.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flowy_editor/service/shortcut_service.dart';
 import 'package:flowy_editor/service/shortcut_service.dart';
 import 'package:flowy_editor/editor_state.dart';
 import 'package:flowy_editor/editor_state.dart';
 
 
-import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
 /// Process selection and cursor
 /// Process selection and cursor
@@ -99,6 +101,92 @@ class FlowySelection extends StatefulWidget {
   State<FlowySelection> createState() => _FlowySelectionState();
   State<FlowySelection> createState() => _FlowySelectionState();
 }
 }
 
 
+/// 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.onPanStart,
+      this.onPanUpdate,
+      this.onPanEnd})
+      : super(key: key);
+
+  @override
+  State<_SelectionGestureDetector> createState() =>
+      _SelectionGestureDetectorState();
+
+  final Widget? child;
+
+  final GestureTapDownCallback? onTapDown;
+  final GestureTapDownCallback? onDoubleTapDown;
+  final GestureDragStartCallback? onPanStart;
+  final GestureDragUpdateCallback? onPanUpdate;
+  final GestureDragEndCallback? onPanEnd;
+}
+
+class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
+  bool _isDoubleTap = false;
+  Timer? _doubleTapTimer;
+  @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 (_isDoubleTap) {
+      _isDoubleTap = false;
+      _doubleTapTimer?.cancel();
+      _doubleTapTimer = null;
+      if (widget.onDoubleTapDown != null) {
+        widget.onDoubleTapDown!(tapDownDetails);
+      }
+    } else {
+      if (widget.onTapDown != null) {
+        widget.onTapDown!(tapDownDetails);
+      }
+
+      _isDoubleTap = true;
+      _doubleTapTimer?.cancel();
+      _doubleTapTimer = Timer(kDoubleTapTimeout, () {
+        _isDoubleTap = false;
+        _doubleTapTimer = null;
+      });
+    }
+  }
+
+  @override
+  void dispose() {
+    _doubleTapTimer?.cancel();
+    super.dispose();
+  }
+}
+
 class _FlowySelectionState extends State<FlowySelection>
 class _FlowySelectionState extends State<FlowySelection>
     with FlowySelectionService, WidgetsBindingObserver {
     with FlowySelectionService, WidgetsBindingObserver {
   final _cursorKey = GlobalKey(debugLabel: 'cursor');
   final _cursorKey = GlobalKey(debugLabel: 'cursor');
@@ -152,27 +240,12 @@ class _FlowySelectionState extends State<FlowySelection>
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    return RawGestureDetector(
-      behavior: HitTestBehavior.translucent,
-      gestures: {
-        PanGestureRecognizer:
-            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
-          () => PanGestureRecognizer(),
-          (recognizer) {
-            recognizer
-              ..onStart = _onPanStart
-              ..onUpdate = _onPanUpdate
-              ..onEnd = _onPanEnd;
-          },
-        ),
-        TapGestureRecognizer:
-            GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
-          () => TapGestureRecognizer(),
-          (recognizer) {
-            recognizer.onTapDown = _onTapDown;
-          },
-        )
-      },
+    return _SelectionGestureDetector(
+      onPanStart: _onPanStart,
+      onPanUpdate: _onPanUpdate,
+      onPanEnd: _onPanEnd,
+      onTapDown: _onTapDown,
+      onDoubleTapDown: _onDoubleTapDown,
       child: widget.child,
       child: widget.child,
     );
     );
   }
   }
@@ -278,6 +351,22 @@ class _FlowySelectionState extends State<FlowySelection>
     return false;
     return false;
   }
   }
 
 
+  void _onDoubleTapDown(TapDownDetails details) {
+    final offset = details.globalPosition;
+    final nodes = getNodesInRange(offset);
+    if (nodes.isEmpty) {
+      editorState.updateCursorSelection(null);
+      return;
+    }
+    final selectable = nodes.first.selectable;
+    if (selectable == null) {
+      editorState.updateCursorSelection(null);
+      return;
+    }
+    editorState
+        .updateCursorSelection(selectable.getWorldBoundaryInOffset(offset));
+  }
+
   void _onTapDown(TapDownDetails details) {
   void _onTapDown(TapDownDetails details) {
     // clear old state.
     // clear old state.
     panStartOffset = null;
     panStartOffset = null;