From e5787090d299d1cbcb4774a801d67653c22eac13 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 5 Aug 2022 10:59:51 +0800 Subject: [PATCH] feat: implement autoscrolling on edge touch --- .../flowy_editor/example/assets/example.json | 56 ++++ .../lib/render/editor/editor_entry.dart | 45 ++- .../lib/render/rich_text/flowy_rich_text.dart | 23 +- .../lib/service/editor_service.dart | 65 ++-- .../lib/service/scroll_service.dart | 65 ++++ .../lib/service/selection_service.dart | 302 +++++++++++------- .../flowy_editor/lib/service/service.dart | 22 +- .../lib/service/toolbar_service.dart | 9 +- 8 files changed, 403 insertions(+), 184 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index c69237f24f..b6fc3467dc 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -241,6 +241,62 @@ "subtype": "number-list", "number": 3 } + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] + }, + { + "type": "text", + "delta": [ + { + "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + } + ] } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart index 650732f9f9..fa32743b02 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart @@ -1,7 +1,8 @@ +import 'package:flutter/material.dart'; + import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flutter/material.dart'; class EditorEntryWidgetBuilder extends NodeWidgetBuilder { @override @@ -31,28 +32,26 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children - .map( - (child) => - editorState.service.renderPluginService.buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ), - ), - ) - .toList(), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), ); } } 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 4ffb4528b4..83d809745c 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 @@ -1,16 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { @override Widget build(NodeWidgetContext context) { @@ -173,11 +173,12 @@ class _FlowyRichTextState extends State with Selectable { } TextSpan get _textSpan => TextSpan( - children: widget.textNode.delta.operations - .whereType() - .map((insert) => RichTextStyle( - attributes: insert.attributes ?? {}, - text: insert.content, - ).toTextSpan()) - .toList(growable: false)); + children: widget.textNode.delta.operations + .whereType() + .map((insert) => RichTextStyle( + attributes: insert.attributes ?? {}, + text: insert.content, + ).toTextSpan()) + .toList(growable: false), + ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index c98b21c17a..d68db05adc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,5 +1,4 @@ -import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -13,10 +12,13 @@ import 'package:flowy_editor/render/rich_text/quoted_text.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/service/scroll_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/service/toolbar_service.dart'; @@ -60,15 +62,25 @@ class FlowyEditor extends StatefulWidget { } class _FlowyEditorState extends State { + late ScrollController _scrollController; + EditorState get editorState => widget.editorState; @override void initState() { super.initState(); + _scrollController = ScrollController()..addListener(_scrollCallback); editorState.service.renderPluginService = _createRenderPlugin(); } + @override + void dispose() { + _scrollController.dispose(); + + super.dispose(); + } + @override void didUpdateWidget(covariant FlowyEditor oldWidget) { super.didUpdateWidget(oldWidget); @@ -80,33 +92,36 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { - return FlowySelection( - key: editorState.service.selectionServiceKey, - editorState: editorState, - child: FlowyInput( - key: editorState.service.inputServiceKey, - editorState: editorState, - child: FlowyKeyboard( - key: editorState.service.keyboardServiceKey, - handlers: [ - ...defaultKeyEventHandler, - ...widget.keyEventHandlers, - ], + return FlowyScroll( + key: editorState.service.scrollServiceKey, + child: FlowySelection( + key: editorState.service.selectionServiceKey, editorState: editorState, - child: FlowyToolbar( - key: editorState.service.toolbarServiceKey, + child: FlowyInput( + key: editorState.service.inputServiceKey, editorState: editorState, - child: editorState.service.renderPluginService.buildPluginWidget( - NodeWidgetContext( - context: context, - node: editorState.document.root, + child: FlowyKeyboard( + key: editorState.service.keyboardServiceKey, + handlers: [ + ...defaultKeyEventHandler, + ...widget.keyEventHandlers, + ], + editorState: editorState, + child: FlowyToolbar( + key: editorState.service.toolbarServiceKey, editorState: editorState, + child: + editorState.service.renderPluginService.buildPluginWidget( + NodeWidgetContext( + context: context, + node: editorState.document.root, + editorState: editorState, + ), + ), ), ), ), - ), - ), - ); + )); } FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin( @@ -116,4 +131,8 @@ class _FlowyEditorState extends State { ...widget.customBuilders, }, ); + + void _scrollCallback() { + debugPrint('scrolling'); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart new file mode 100644 index 0000000000..c3a0a6fedc --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart @@ -0,0 +1,65 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +mixin FlowyScrollService on State { + double get dy; + + void scrollTo(double dy); + + RenderObject? scrollRenderObject(); +} + +class FlowyScroll extends StatefulWidget { + const FlowyScroll({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + State createState() => _FlowyScrollState(); +} + +class _FlowyScrollState extends State with FlowyScrollService { + final _scrollController = ScrollController(); + final _scrollViewKey = GlobalKey(); + + @override + double get dy => _scrollController.position.pixels; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerSignal: _onPointerSignal, + child: SingleChildScrollView( + key: _scrollViewKey, + physics: const NeverScrollableScrollPhysics(), + controller: _scrollController, + child: widget.child, + ), + ); + } + + @override + void scrollTo(double dy) { + _scrollController.position.jumpTo( + dy.clamp( + _scrollController.position.minScrollExtent, + _scrollController.position.maxScrollExtent, + ), + ); + } + + void _onPointerSignal(PointerSignalEvent event) { + if (event is PointerScrollEvent) { + final dy = (_scrollController.position.pixels + event.scrollDelta.dy); + scrollTo(dy); + } + } + + @override + RenderObject? scrollRenderObject() { + return _scrollViewKey.currentContext?.findRenderObject(); + } +} 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 59632773e5..b879ea419a 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,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -99,102 +100,18 @@ 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'); final List _selectionOverlays = []; final List _cursorOverlays = []; + OverlayEntry? _debugOverlay; /// [Pan] and [Tap] must be mutually exclusive. /// Pan Offset? panStartOffset; + double? panStartScrollDy; Offset? panEndOffset; /// Tap @@ -259,7 +176,7 @@ class _FlowySelectionState extends State @override void updateSelection(Selection selection) { _rects.clear(); - _clearSelection(); + clearSelection(); // cursor if (selection.isCollapsed) { @@ -273,7 +190,19 @@ class _FlowySelectionState extends State @override void clearSelection() { - _clearSelection(); + currentSelection = null; + currentSelectedNodes.value = []; + + // clear selection + _selectionOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + // clear cursors + _cursorOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + // clear toolbar + editorState.service.toolbarService?.hide(); } @override @@ -325,7 +254,7 @@ class _FlowySelectionState extends State } } for (final child in node.children) { - result.addAll(computeNodesInRange(child, start, end)); + result.addAll(_computeNodesInRange(child, start, end)); } return result; } @@ -411,12 +340,24 @@ class _FlowySelectionState extends State clearSelection(); panStartOffset = details.globalPosition; + panStartScrollDy = editorState.service.scrollService?.dy; + + debugPrint('[_onPanStart] panStartOffset = $panStartOffset'); } void _onPanUpdate(DragUpdateDetails details) { - panEndOffset = details.globalPosition; + if (panStartOffset == null || panStartScrollDy == null) { + return; + } - final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + panEndOffset = details.globalPosition; + final dy = editorState.service.scrollService?.dy; + var panStartOffsetWithScrollDyGap = panStartOffset!; + if (dy != null) { + panStartOffsetWithScrollDyGap = + panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy); + } + final nodes = getNodesInRange(panStartOffsetWithScrollDyGap, panEndOffset!); if (nodes.isEmpty) { return; } @@ -427,40 +368,30 @@ class _FlowySelectionState extends State if (first != null && last != null) { bool isDownward; if (first == last) { - isDownward = panStartOffset!.dx < panEndOffset!.dx; + isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx; } else { - isDownward = panStartOffset!.dy < panEndOffset!.dy; + isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy; } - final start = - first.getSelectionInRange(panStartOffset!, panEndOffset!).start; - final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; + 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); debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); editorState.service.selectionService.updateSelection(selection); } + + _scrollUpOrDownIfNeeded(panEndOffset!); + _showDebugLayerIfNeeded(); } void _onPanEnd(DragEndDetails details) { // do nothing } - void _clearSelection() { - currentSelection = null; - currentSelectedNodes.value = []; - - // clear selection - _selectionOverlays - ..forEach((overlay) => overlay.remove()) - ..clear(); - // clear cursors - _cursorOverlays - ..forEach((overlay) => overlay.remove()) - ..clear(); - // clear toolbar - editorState.service.toolbarService?.hide(); - } - void _updateSelection(Selection selection) { final nodes = _selectedNodesInSelection(editorState.document.root, selection); @@ -554,12 +485,12 @@ class _FlowySelectionState extends State if (rect != null) { _rects.add(_transformRectToGlobal(selectable!, rect)); final cursor = OverlayEntry( - builder: ((context) => CursorWidget( - key: _cursorKey, - rect: rect, - color: widget.cursorColor, - layerLink: node.layerLink, - )), + builder: (context) => CursorWidget( + key: _cursorKey, + rect: rect, + color: widget.cursorColor, + layerLink: node.layerLink, + ), ); _cursorOverlays.add(cursor); Overlay.of(context)?.insertAll(_cursorOverlays); @@ -584,4 +515,139 @@ class _FlowySelectionState extends State } return result; } + + void _scrollUpOrDownIfNeeded(Offset offset) { + final dy = editorState.service.scrollService?.dy; + if (dy == null) { + assert(false, 'Dy could not be null'); + return; + } + final topLimit = MediaQuery.of(context).size.height * 0.2; + final bottomLimit = MediaQuery.of(context).size.height * 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) { + // up + editorState.service.scrollService?.scrollTo(dy - distance); + } else if (offset.dy >= bottomLimit) { + //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; + } + } + } +} + +/// 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(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index d9920423ae..e36312d7f3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,8 +1,10 @@ +import 'package:flutter/material.dart'; + import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/toolbar_service.dart'; +import 'package:flowy_editor/service/scroll_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; class FlowyService { // selection service @@ -31,10 +33,20 @@ class FlowyService { // toolbar service final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); - ToolbarService? get toolbarService { + FlowyToolbarService? get toolbarService { if (toolbarServiceKey.currentState != null && - toolbarServiceKey.currentState is ToolbarService) { - return toolbarServiceKey.currentState! as ToolbarService; + toolbarServiceKey.currentState is FlowyToolbarService) { + return toolbarServiceKey.currentState! as FlowyToolbarService; + } + return null; + } + + // scroll service + final scrollServiceKey = GlobalKey(debugLabel: 'flowy_scroll_service'); + FlowyScrollService? get scrollService { + if (scrollServiceKey.currentState != null && + scrollServiceKey.currentState is FlowyScrollService) { + return scrollServiceKey.currentState! as FlowyScrollService; } return null; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart index feb293aad4..f2026acb23 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -1,8 +1,9 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/render/selection/toolbar_widget.dart'; import 'package:flutter/material.dart'; -mixin ToolbarService { +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/render/selection/toolbar_widget.dart'; + +mixin FlowyToolbarService { /// Show the toolbar widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); @@ -24,7 +25,7 @@ class FlowyToolbar extends StatefulWidget { State createState() => _FlowyToolbarState(); } -class _FlowyToolbarState extends State with ToolbarService { +class _FlowyToolbarState extends State with FlowyToolbarService { OverlayEntry? _toolbarOverlay; @override