From 2a09f69bec069cec085db61b99c6c3c04ba0121e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 18:06:54 +0800 Subject: [PATCH] feat: double tap on text --- .../lib/render/rich_text/flowy_rich_text.dart | 10 ++ .../lib/render/selection/selectable.dart | 4 + .../lib/service/selection_service.dart | 133 +++++++++++++++--- 3 files changed, 125 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 4731542ae2..122b65991e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -108,6 +108,16 @@ class _FlowyRichTextState extends State with Selectable { 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 List getRectsInSelection(Selection selection) { assert(pathEquals(selection.start.path, selection.end.path) && diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index b677b2f47c..bc32706aa0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -21,6 +21,10 @@ mixin Selectable on State { /// /// The return result must be an offset of the local coordinate system. Position getPositionInOffset(Offset start); + Selection? getWorldBoundaryInOffset(Offset start) { + return null; + } + Rect getCursorRectInPosition(Position position); Offset localToGlobal(Offset offset); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 3cfd1fd3f7..43b77baeaf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/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/position.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/extensions/object_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/editor_state.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; /// Process selection and cursor @@ -99,6 +101,92 @@ class FlowySelection extends StatefulWidget { State 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(), + (recognizer) { + recognizer + ..onStart = widget.onPanStart + ..onUpdate = widget.onPanUpdate + ..onEnd = widget.onPanEnd; + }, + ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => 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 with FlowySelectionService, WidgetsBindingObserver { final _cursorKey = GlobalKey(debugLabel: 'cursor'); @@ -152,27 +240,12 @@ class _FlowySelectionState extends State @override Widget build(BuildContext context) { - return RawGestureDetector( - behavior: HitTestBehavior.translucent, - gestures: { - PanGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(), - (recognizer) { - recognizer - ..onStart = _onPanStart - ..onUpdate = _onPanUpdate - ..onEnd = _onPanEnd; - }, - ), - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (recognizer) { - recognizer.onTapDown = _onTapDown; - }, - ) - }, + return _SelectionGestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onTapDown: _onTapDown, + onDoubleTapDown: _onDoubleTapDown, child: widget.child, ); } @@ -278,6 +351,22 @@ class _FlowySelectionState extends State 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) { // clear old state. panStartOffset = null;