diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da33d3bb..8cf08febe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 2.2.0 +* feat: support customizing the mobile magnifier by @LucasXu0 in ([#625](https://github.com/AppFlowy-IO/appflowy-editor/pull/625)) +* feat: support keep editor focus on mobile by @LucasXu0 in ([#628](https://github.com/AppFlowy-IO/appflowy-editor/pull/628)) +* feat: support clicking the selection area to disable floating toolbar by @LucasXu0 in ([#632](https://github.com/AppFlowy-IO/appflowy-editor/pull/632)) +* feat: adding an ability to have a link check before embedding by @johansutrisno in ([#603](https://github.com/AppFlowy-IO/appflowy-editor/pull/603)) +* feat: Add markdown link syntax formatting by @jazima in ([#618](https://github.com/AppFlowy-IO/appflowy-editor/pull/618)) +* feat: Table navigation using TAB key by @AnsahMohammad in ([#627](https://github.com/AppFlowy-IO/appflowy-editor/pull/627)) +* feat: improve android selection gesture by @LucasXu0 in ([#647](https://github.com/AppFlowy-IO/appflowy-editor/pull/647)) +* feat: improve ios touch gesture by @LucasXu0 in ([#648](https://github.com/AppFlowy-IO/appflowy-editor/pull/648)) +* fix: indent/outdent doesn't work in not collapsed selection by @LucasXu0 in ([#626](https://github.com/AppFlowy-IO/appflowy-editor/pull/626)) +* fix: renamed duplicated shortcut key by @AnsahMohammad in ([#629](https://github.com/AppFlowy-IO/appflowy-editor/pull/629)) +* fix: todo list lost focus by @LucasXu0 in ([#633](https://github.com/AppFlowy-IO/appflowy-editor/pull/633)) +* fix: resolve dead loop in node_iterator toList by @Linij in ([#623](https://github.com/AppFlowy-IO/appflowy-editor/pull/623)) +* fix: active hover on upload image by @johansutrisno in ([#597](https://github.com/AppFlowy-IO/appflowy-editor/pull/597)) +* fix:text_decoration_mobile_toolbar_padding by @q200892907 in ([#621](https://github.com/AppFlowy-IO/appflowy-editor/pull/621)) +* fix: android 14 issues by @LucasXu0 in ([#649](https://github.com/AppFlowy-IO/appflowy-editor/pull/649)) + ## 2.1.0 * feat: show magnifier when dragging the handlers by @LucasXu0 in ([#601](https://github.com/AppFlowy-IO/appflowy-editor/pull/601)) * feat: refactor keyboard height observer to support multiple listeners by @LucasXu0 in ([#602](https://github.com/AppFlowy-IO/appflowy-editor/pull/602)) diff --git a/example/lib/pages/customize_theme_for_editor.dart b/example/lib/pages/customize_theme_for_editor.dart index 224365762..273ea7283 100644 --- a/example/lib/pages/customize_theme_for_editor.dart +++ b/example/lib/pages/customize_theme_for_editor.dart @@ -156,6 +156,7 @@ class _CustomizeThemeForEditorState extends State { ? const EdgeInsets.only(left: 200, right: 200) : const EdgeInsets.symmetric(horizontal: 20), cursorColor: Colors.green, + dragHandleColor: Colors.green, selectionColor: Colors.green.withOpacity(0.5), textStyleConfiguration: TextStyleConfiguration( text: GoogleFonts.poppins( diff --git a/example/lib/pages/mobile_editor.dart b/example/lib/pages/mobile_editor.dart index e5fe8efed..6ad89a76e 100644 --- a/example/lib/pages/mobile_editor.dart +++ b/example/lib/pages/mobile_editor.dart @@ -112,6 +112,7 @@ class _MobileEditorState extends State { EditorStyle _buildMobileEditorStyle() { return EditorStyle.mobile( cursorColor: const Color.fromARGB(255, 134, 46, 247), + dragHandleColor: const Color.fromARGB(255, 134, 46, 247), selectionColor: const Color.fromARGB(50, 134, 46, 247), textStyleConfiguration: TextStyleConfiguration( text: GoogleFonts.poppins( diff --git a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart index 5ba3727fe..318024b2c 100644 --- a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart'; import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/service/selection/selection_gesture.dart'; import 'package:flutter/material.dart' hide Overlay, OverlayEntry; @@ -191,6 +192,30 @@ class _DesktopSelectionServiceWidgetState return selectable.getPositionInOffset(offset); } + @override + Selection? onPanStart( + DragStartDetails details, + MobileSelectionDragMode mode, + ) { + throw UnimplementedError(); + } + + @override + Selection? onPanUpdate( + DragUpdateDetails details, + MobileSelectionDragMode mode, + ) { + throw UnimplementedError(); + } + + @override + void onPanEnd( + DragEndDetails details, + MobileSelectionDragMode mode, + ) { + throw UnimplementedError(); + } + void _onTapDown(TapDownDetails details) { _clearContextMenu(); diff --git a/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart b/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart index 1fca6d854..4b16452de 100644 --- a/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/mobile_selection_service.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'dart:io'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_magnifier.dart'; -import 'package:appflowy_editor/src/flutter/overlay.dart'; -import 'package:appflowy_editor/src/render/selection/mobile_selection_widget.dart'; +import 'package:appflowy_editor/src/render/selection/mobile_basic_handle.dart'; +import 'package:appflowy_editor/src/render/selection/mobile_collapsed_handle.dart'; +import 'package:appflowy_editor/src/render/selection/mobile_selection_handle.dart'; import 'package:appflowy_editor/src/service/selection/mobile_selection_gesture.dart'; import 'package:flutter/material.dart' hide Overlay, OverlayEntry; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; /// only used in mobile @@ -62,8 +65,6 @@ class _MobileSelectionServiceWidgetState implements AppFlowySelectionService { @override final List selectionRects = []; - final List _selectionAreas = []; - final List _cursorAreas = []; @override ValueNotifier currentSelection = ValueNotifier(null); @@ -81,6 +82,8 @@ class _MobileSelectionServiceWidgetState MobileSelectionDragMode dragMode = MobileSelectionDragMode.none; + bool updateSelectionByTapUp = false; + late EditorState editorState = Provider.of( context, listen: false, @@ -106,19 +109,23 @@ class _MobileSelectionServiceWidgetState @override Widget build(BuildContext context) { return MobileSelectionGestureDetector( - onPanStart: _onPanStart, - onPanUpdate: _onPanUpdate, - onPanEnd: _onPanEnd, onTapUp: _onTapUp, onDoubleTapUp: _onDoubleTapUp, onTripleTapUp: _onTripleTapUp, onLongPressStart: _onLongPressStart, - onLongPressMoveUpdate: _onLongPressMoveUpdate, - onLongPressEnd: _onLongPressEnd, + // onLongPressMoveUpdate: _onLongPressMoveUpdate, + // onLongPressEnd: _onLongPressEnd, child: Stack( children: [ widget.child, + + // magnifier for zoom in the text. if (widget.showMagnifier) _buildMagnifier(), + + // the handles for expanding the selection area. + _buildLeftHandle(), + _buildRightHandle(), + _buildCollapsedHandle(), ], ), ); @@ -141,6 +148,96 @@ class _MobileSelectionServiceWidgetState ); } + Widget _buildCollapsedHandle() { + return ValueListenableBuilder( + valueListenable: editorState.selectionNotifier, + builder: (context, selection, _) { + if (selection == null || + !selection.isCollapsed || + editorState.selectionUpdateReason != + SelectionUpdateReason.uiEvent) { + return const SizedBox.shrink(); + } + + selection = selection.normalized; + + final node = editorState.getNodeAtPath(selection.start.path); + final selectable = node?.selectable; + var rect = selectable?.getCursorRectInPosition( + selection.start, + shiftWithBaseOffset: true, + ); + + if (node == null || rect == null) { + return const SizedBox.shrink(); + } + + final editorStyle = editorState.editorStyle; + return MobileCollapsedHandle( + layerLink: node.layerLink, + rect: rect, + handleColor: editorStyle.dragHandleColor, + handleWidth: editorStyle.mobileDragHandleWidth, + handleBallWidth: editorStyle.mobileDragHandleBallSize.width, + enableHapticFeedbackOnAndroid: + editorStyle.enableHapticFeedbackOnAndroid, + ); + }, + ); + } + + Widget _buildLeftHandle() { + return _buildHandle(HandleType.left); + } + + Widget _buildRightHandle() { + return _buildHandle(HandleType.right); + } + + Widget _buildHandle(HandleType handleType) { + if (![HandleType.left, HandleType.right].contains(handleType)) { + throw ArgumentError('showLeftHandle and showRightHandle cannot be same.'); + } + + return ValueListenableBuilder( + valueListenable: editorState.selectionNotifier, + builder: (context, selection, _) { + if (selection == null || selection.isCollapsed) { + return const SizedBox.shrink(); + } + + selection = selection.normalized; + + final node = editorState.getNodeAtPath( + handleType == HandleType.left + ? selection.start.path + : selection.end.path, + ); + final selectable = node?.selectable; + final rects = selectable?.getRectsInSelection( + selection, + shiftWithBaseOffset: true, + ); + + if (node == null || rects == null || rects.isEmpty) { + return const SizedBox.shrink(); + } + + final editorStyle = editorState.editorStyle; + return MobileSelectionHandle( + layerLink: node.layerLink, + rect: handleType == HandleType.left ? rects.first : rects.last, + handleType: handleType, + handleColor: editorStyle.dragHandleColor, + handleWidth: editorStyle.mobileDragHandleWidth, + handleBallWidth: editorStyle.mobileDragHandleBallSize.width, + enableHapticFeedbackOnAndroid: + editorStyle.enableHapticFeedbackOnAndroid, + ); + }, + ); + } + @override void updateSelection(Selection? selection) { if (currentSelection.value == selection) { @@ -175,23 +272,13 @@ class _MobileSelectionServiceWidgetState _clearSelection(); } - void _clearSelection() { - clearCursor(); - // clear selection areas - _selectionAreas - ..forEach((overlay) => overlay.remove()) - ..clear(); - // clear cursor areas - _selectionAreas.clear(); - selectionRects.clear(); - } - @override void clearCursor() { - // clear cursor areas - _cursorAreas - ..forEach((overlay) => overlay.remove()) - ..clear(); + _clearSelection(); + } + + void _clearSelection() { + selectionRects.clear(); } @override @@ -290,6 +377,7 @@ class _MobileSelectionServiceWidgetState editorState.updateSelectionWithReason( Selection.collapsed(position), + reason: SelectionUpdateReason.uiEvent, extraInfo: null, ); } @@ -311,10 +399,6 @@ class _MobileSelectionServiceWidgetState void _onTripleTapUp(TapUpDetails details) { final offset = details.globalPosition; - // if (_isClickOnSelectionArea(offset)) { - // appFlowyEditorOnTapSelectionArea.add(0); - // return; - // } final node = getNodeInOffset(offset); final selectable = node?.selectable; if (selectable == null) { @@ -328,45 +412,35 @@ class _MobileSelectionServiceWidgetState updateSelection(selection); } - void _onPanStart(DragStartDetails details) { + @override + Selection? onPanStart( + DragStartDetails details, + MobileSelectionDragMode mode, + ) { _panStartOffset = details.globalPosition.translate(-3.0, 0); _panStartScrollDy = editorState.service.scrollService?.dy; - final position = details.globalPosition; final selection = editorState.selection; _panStartSelection = selection; - if (selection == null) { - dragMode = MobileSelectionDragMode.none; - } else if (selection.isCollapsed && - _isOverlayOnHandler( - position, - MobileSelectionHandlerType.cursorHandler, - )) { - dragMode = MobileSelectionDragMode.cursor; - } else if (_isOverlayOnHandler( - position, - MobileSelectionHandlerType.leftHandler, - )) { - dragMode = MobileSelectionDragMode.leftSelectionHandler; - } else if (_isOverlayOnHandler( - position, - MobileSelectionHandlerType.rightHandler, - )) { - dragMode = MobileSelectionDragMode.rightSelectionHandler; - } else { - dragMode = MobileSelectionDragMode.none; - } + + dragMode = mode; + + return selection; } - void _onPanUpdate(DragUpdateDetails details) { + @override + Selection? onPanUpdate( + DragUpdateDetails details, + MobileSelectionDragMode mode, + ) { if (_panStartOffset == null || _panStartScrollDy == null) { - return; + return null; } // only support selection mode now. final selection = editorState.selection; if (selection == null || dragMode == MobileSelectionDragMode.none) { - return; + return null; } final panEndOffset = details.globalPosition; @@ -380,32 +454,34 @@ class _MobileSelectionServiceWidgetState ?.getSelectionInRange(panStartOffset, panEndOffset) .end; + Selection? newSelection; + if (end != null) { if (dragMode == MobileSelectionDragMode.leftSelectionHandler) { - updateSelection( - Selection( - start: _panStartSelection!.normalized.end, - end: end, - ).normalized, - ); + newSelection = Selection( + start: _panStartSelection!.normalized.end, + end: end, + ).normalized; } else if (dragMode == MobileSelectionDragMode.rightSelectionHandler) { - updateSelection( - Selection( - start: _panStartSelection!.normalized.start, - end: end, - ).normalized, - ); + newSelection = Selection( + start: _panStartSelection!.normalized.start, + end: end, + ).normalized; } else if (dragMode == MobileSelectionDragMode.cursor) { - updateSelection( - Selection.collapsed(end), - ); + newSelection = Selection.collapsed(end); } - _lastPanOffset.value = panEndOffset; } + + if (newSelection != null) { + updateSelection(newSelection); + } + + return newSelection; } - void _onPanEnd(DragEndDetails details) { + @override + void onPanEnd(DragEndDetails details, MobileSelectionDragMode mode) { // do nothing _lastPanOffset.value = null; @@ -413,55 +489,37 @@ class _MobileSelectionServiceWidgetState dragMode = MobileSelectionDragMode.none; editorState.updateSelectionWithReason( editorState.selection, + reason: SelectionUpdateReason.uiEvent, extraInfo: null, ); } void _onLongPressStart(LongPressStartDetails details) { - clearSelection(); - - // clear old state. - _panStartOffset = null; - - final position = getPositionInOffset(details.globalPosition); - if (position == null) { + if (!Platform.isAndroid) { return; } - _lastPanOffset.value = details.globalPosition; - - dragMode = MobileSelectionDragMode.cursor; - updateSelection( - Selection.collapsed(position), - ); - } - - void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - final panEndOffset = details.globalPosition; - final position = getNodeInOffset(panEndOffset) - ?.selectable - ?.getPositionInOffset(panEndOffset); - - if (position == null) { + // on Android, long press to select a word. + final offset = details.globalPosition; + if (_isClickOnSelectionArea(offset)) { + appFlowyEditorOnTapSelectionArea.add(0); + return; + } + final node = getNodeInOffset(offset); + final selection = node?.selectable?.getWordBoundaryInOffset(offset); + if (selection == null) { + clearSelection(); return; } - _lastPanOffset.value = panEndOffset; - updateSelection( - Selection.collapsed(position), - ); - } - - void _onLongPressEnd(LongPressEndDetails details) { - _lastPanOffset.value = null; + if (editorState.editorStyle.enableHapticFeedbackOnAndroid) { + HapticFeedback.mediumImpact(); + } - dragMode = MobileSelectionDragMode.none; - editorState.updateSelectionWithReason( - editorState.selection, - extraInfo: null, - ); + updateSelection(selection); } + // delete this function in the future. void _updateSelectionAreas(Selection selection) { final nodes = editorState.getNodesInSelection(selection); @@ -509,39 +567,14 @@ class _MobileSelectionServiceWidgetState newSelection, shiftWithBaseOffset: true, ); - for (var (j, rect) in rects.indexed) { + for (final rect in rects) { final selectionRect = selectable.transformRectToGlobal( rect, shiftWithBaseOffset: true, ); selectionRects.add(selectionRect); - final showLeftHandler = i == 0 && j == 0; - final showRightHandler = - i == backwardNodes.length - 1 && j == rects.length - 1; - if (rect.width <= 0) { - rect = Rect.fromLTWH(rect.left, rect.top, 8.0, rect.height); - } - final overlay = OverlayEntry( - builder: (context) => MobileSelectionWidget( - color: Colors.transparent, - layerLink: node.layerLink, - rect: rect, - showLeftHandler: showLeftHandler, - showRightHandler: showRightHandler, - handlerColor: editorState.editorStyle.cursorColor, - handlerWidth: editorState.editorStyle.mobileDragHandleWidth, - handlerBallWidth: - editorState.editorStyle.mobileDragHandleBallSize.width, - ), - ); - _selectionAreas.add(overlay); } } - - final overlay = Overlay.of(context); - overlay?.insertAll( - _selectionAreas, - ); } Node? _getNodeInOffset( @@ -578,60 +611,6 @@ class _MobileSelectionServiceWidgetState return node; } - bool _isOverlayOnHandler(Offset point, MobileSelectionHandlerType type) { - final selection = editorState.selection; - if (selection == null) { - return false; - } - - SelectableMixin? selectable; - Rect? rect; - - switch (type) { - case MobileSelectionHandlerType.leftHandler: - case MobileSelectionHandlerType.cursorHandler: - selectable = - editorState.getNodeAtPath(selection.start.path)?.selectable; - if (selectable == null) { - return false; - } - rect = selectable.getCursorRectInPosition( - selection.start, - shiftWithBaseOffset: true, - ); - if (rect == null) { - return false; - } - break; - case MobileSelectionHandlerType.rightHandler: - selectable = editorState.getNodeAtPath(selection.end.path)?.selectable; - if (selectable == null) { - return false; - } - rect = selectable.getCursorRectInPosition( - selection.end, - shiftWithBaseOffset: true, - ); - if (rect == null) { - return false; - } - break; - } - - const extend = 20.0; - final handlerRect = selectable.transformRectToGlobal( - Rect.fromLTWH( - rect.left - extend, - rect.top - extend, - extend * 2, - rect.height + 2 * extend, - ), - shiftWithBaseOffset: true, - ); - - return handlerRect.contains(point); - } - bool _isClickOnSelectionArea(Offset point) { for (final rect in selectionRects) { if (rect.contains(point)) { diff --git a/lib/src/editor/editor_component/service/selection_service.dart b/lib/src/editor/editor_component/service/selection_service.dart index f5834637d..fa42d6080 100644 --- a/lib/src/editor/editor_component/service/selection_service.dart +++ b/lib/src/editor/editor_component/service/selection_service.dart @@ -1,6 +1,7 @@ import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/location/position.dart'; import 'package:appflowy_editor/src/core/location/selection.dart'; +import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart'; import 'package:flutter/material.dart' hide Overlay, OverlayEntry; /// [AppFlowySelectionService] is responsible for processing @@ -66,6 +67,20 @@ abstract class AppFlowySelectionService { void registerGestureInterceptor(SelectionGestureInterceptor interceptor); void unregisterGestureInterceptor(String key); + + /// The functions below are only for mobile. + Selection? onPanStart( + DragStartDetails details, + MobileSelectionDragMode mode, + ); + Selection? onPanUpdate( + DragUpdateDetails details, + MobileSelectionDragMode mode, + ); + void onPanEnd( + DragEndDetails details, + MobileSelectionDragMode mode, + ); } class SelectionGestureInterceptor { diff --git a/lib/src/editor/editor_component/service/selection_service_widget.dart b/lib/src/editor/editor_component/service/selection_service_widget.dart index 884b88a45..cfd9aef0e 100644 --- a/lib/src/editor/editor_component/service/selection_service_widget.dart +++ b/lib/src/editor/editor_component/service/selection_service_widget.dart @@ -93,4 +93,25 @@ class _SelectionServiceWidgetState extends State @override void updateSelection(Selection? selection) => forward.updateSelection(selection); + + @override + Selection? onPanStart( + DragStartDetails details, + MobileSelectionDragMode mode, + ) => + forward.onPanStart(details, mode); + + @override + Selection? onPanUpdate( + DragUpdateDetails details, + MobileSelectionDragMode mode, + ) => + forward.onPanUpdate(details, mode); + + @override + void onPanEnd( + DragEndDetails details, + MobileSelectionDragMode mode, + ) => + forward.onPanEnd(details, mode); } diff --git a/lib/src/editor/editor_component/style/editor_style.dart b/lib/src/editor/editor_component/style/editor_style.dart index cb65f3236..163eebb1a 100644 --- a/lib/src/editor/editor_component/style/editor_style.dart +++ b/lib/src/editor/editor_component/style/editor_style.dart @@ -10,6 +10,7 @@ class EditorStyle { const EditorStyle({ required this.padding, required this.cursorColor, + required this.dragHandleColor, required this.selectionColor, required this.textStyleConfiguration, required this.textSpanDecorator, @@ -17,44 +18,54 @@ class EditorStyle { this.mobileDragHandleBallSize = const Size(8, 8), this.mobileDragHandleWidth = 2.0, this.defaultTextDirection, + this.enableHapticFeedbackOnAndroid = true, }); - /// The padding of the editor. + // The padding of the editor. final EdgeInsets padding; - /// The cursor color + // The cursor color final Color cursorColor; - /// The selection color + // The drag handle color + // only works on mobile + // the drag handle color will be ignored on Android. + final Color dragHandleColor; + + // The selection color final Color selectionColor; - /// Customize the text style of the editor. - /// - /// All the text-based components will use this configuration to build their - /// text style. - /// - /// Notes, this configuration is only for the common config of text style and - /// it maybe override if the text block has its own [BlockComponentConfiguration]. + // Customize the text style of the editor. + // + // All the text-based components will use this configuration to build their + // text style. + // + // Notes, this configuration is only for the common config of text style and + // it maybe override if the text block has its own [BlockComponentConfiguration]. final TextStyleConfiguration textStyleConfiguration; - /// Customize the built-in or custom text span. - /// - /// For example, you can add a custom text span for the mention text - /// or override the built-in text span. + // Customize the built-in or custom text span. + // + // For example, you can add a custom text span for the mention text + // or override the built-in text span. final TextSpanDecoratorForAttribute? textSpanDecorator; final String? defaultTextDirection; - /// The size of the magnifier. - /// Only works on mobile. + // The size of the magnifier. + // Only works on mobile. final Size magnifierSize; - /// mobile drag handler size. - /// /// Only works on mobile. + // mobile drag handler size. + // Only works on mobile. final Size mobileDragHandleBallSize; final double mobileDragHandleWidth; + // only works on android + // enable haptic feedback when updating selection by dragging the drag handler + final bool enableHapticFeedbackOnAndroid; + const EditorStyle.desktop({ EdgeInsets? padding, Color? cursorColor, @@ -74,11 +85,14 @@ class EditorStyle { textSpanDecorator ?? defaultTextSpanDecoratorForAttribute, magnifierSize = Size.zero, mobileDragHandleBallSize = Size.zero, - mobileDragHandleWidth = 0.0; + mobileDragHandleWidth = 0.0, + enableHapticFeedbackOnAndroid = false, + dragHandleColor = Colors.transparent; const EditorStyle.mobile({ EdgeInsets? padding, Color? cursorColor, + Color? dragHandleColor, Color? selectionColor, TextStyleConfiguration? textStyleConfiguration, TextSpanDecoratorForAttribute? textSpanDecorator, @@ -86,8 +100,10 @@ class EditorStyle { this.magnifierSize = const Size(72, 48), this.mobileDragHandleBallSize = const Size(8, 8), this.mobileDragHandleWidth = 2.0, + this.enableHapticFeedbackOnAndroid = true, }) : padding = padding ?? const EdgeInsets.symmetric(horizontal: 20), cursorColor = cursorColor ?? const Color(0xFF00BCF0), + dragHandleColor = dragHandleColor ?? const Color(0xFF00BCF0), selectionColor = selectionColor ?? const Color.fromARGB(53, 111, 201, 231), textStyleConfiguration = textStyleConfiguration ?? @@ -100,19 +116,32 @@ class EditorStyle { EditorStyle copyWith({ EdgeInsets? padding, Color? cursorColor, + Color? dragHandleColor, Color? selectionColor, TextStyleConfiguration? textStyleConfiguration, TextSpanDecoratorForAttribute? textSpanDecorator, String? defaultTextDirection, + Size? magnifierSize, + Size? mobileDragHandleBallSize, + double? mobileDragHandleWidth, + bool? enableHapticFeedbackOnAndroid, }) { return EditorStyle( padding: padding ?? this.padding, cursorColor: cursorColor ?? this.cursorColor, + dragHandleColor: dragHandleColor ?? this.dragHandleColor, selectionColor: selectionColor ?? this.selectionColor, textStyleConfiguration: textStyleConfiguration ?? this.textStyleConfiguration, textSpanDecorator: textSpanDecorator ?? this.textSpanDecorator, defaultTextDirection: defaultTextDirection, + magnifierSize: magnifierSize ?? this.magnifierSize, + mobileDragHandleBallSize: + mobileDragHandleBallSize ?? this.mobileDragHandleBallSize, + mobileDragHandleWidth: + mobileDragHandleWidth ?? this.mobileDragHandleWidth, + enableHapticFeedbackOnAndroid: + enableHapticFeedbackOnAndroid ?? this.enableHapticFeedbackOnAndroid, ); } } diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart index bc90e3980..0ecc3c132 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/text_decoration_mobile_toolbar_item_v2.dart @@ -90,7 +90,12 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> { isSelected: isSelected, onPressed: () { setState(() { - widget.editorState.toggleAttribute(currentDecoration.name); + widget.editorState.toggleAttribute( + currentDecoration.name, + selectionExtraInfo: { + selectionExtraInfoDoNotAttachTextService: true, + }, + ); }); }, ); diff --git a/lib/src/editor/toolbar/mobile/utils/keyboard_height_observer.dart b/lib/src/editor/toolbar/mobile/utils/keyboard_height_observer.dart index a58a214db..f6365458d 100644 --- a/lib/src/editor/toolbar/mobile/utils/keyboard_height_observer.dart +++ b/lib/src/editor/toolbar/mobile/utils/keyboard_height_observer.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; import 'package:keyboard_height_plugin/keyboard_height_plugin.dart'; typedef KeyboardHeightCallback = void Function(double height); @@ -5,7 +8,14 @@ typedef KeyboardHeightCallback = void Function(double height); // the KeyboardHeightPlugin only accepts one listener, so we need to create a // singleton class to manage the multiple listeners. class KeyboardHeightObserver { + static int androidSDKVersion = -1; + KeyboardHeightObserver._() { + if (Platform.isAndroid && androidSDKVersion == -1) { + DeviceInfoPlugin().androidInfo.then( + (value) => androidSDKVersion = value.version.sdkInt, + ); + } _keyboardHeightPlugin.onKeyboardHeightChanged((height) { currentKeyboardHeight = height; notify(height); @@ -32,6 +42,13 @@ class KeyboardHeightObserver { } void notify(double height) { + // the keyboard height will notify twice with the same value on Android 14 + if (androidSDKVersion == 34) { + if (height == 0 && currentKeyboardHeight == 0) { + return; + } + } + for (final listener in _listeners) { listener(height); } diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 3948bc9b2..963560f21 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -300,10 +300,8 @@ class EditorState { selectionExtraInfo = transaction.selectionExtraInfo; } selection = transaction.afterSelection; - _selectionUpdateReason = SelectionUpdateReason.uiEvent; } - // TODO: execute this line after the UI has been updated. completer.complete(); return completer.future; diff --git a/lib/src/render/selection/mobile_basic_handle.dart b/lib/src/render/selection/mobile_basic_handle.dart new file mode 100644 index 000000000..aa800a347 --- /dev/null +++ b/lib/src/render/selection/mobile_basic_handle.dart @@ -0,0 +1,350 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +enum HandleType { + none, + left, + right, + collapsed; + + MobileSelectionDragMode get dragMode { + switch (this) { + case HandleType.none: + throw UnsupportedError('Unsupported handle type'); + case HandleType.left: + return MobileSelectionDragMode.leftSelectionHandler; + case HandleType.right: + return MobileSelectionDragMode.rightSelectionHandler; + case HandleType.collapsed: + return MobileSelectionDragMode.cursor; + } + } + + CrossAxisAlignment get crossAxisAlignment { + switch (this) { + case HandleType.none: + throw UnsupportedError('Unsupported handle type'); + case HandleType.left: + return CrossAxisAlignment.end; + case HandleType.right: + return CrossAxisAlignment.start; + case HandleType.collapsed: + return CrossAxisAlignment.center; + } + } +} + +abstract class _IDragHandle extends StatelessWidget { + const _IDragHandle({ + super.key, + required this.handleHeight, + this.handleColor = Colors.black, + this.handleWidth = 2.0, + this.handleBallWidth = 6.0, + this.debugPaintSizeEnabled = false, + required this.handleType, + }); + + final Color handleColor; + final double handleWidth; + final double handleHeight; + final double handleBallWidth; + final HandleType handleType; + final bool debugPaintSizeEnabled; +} + +class DragHandle extends _IDragHandle { + const DragHandle({ + super.key, + required super.handleHeight, + super.handleColor, + super.handleWidth, + super.handleBallWidth, + required super.handleType, + super.debugPaintSizeEnabled, + }); + + @override + Widget build(BuildContext context) { + Widget child; + + if (Platform.isIOS) { + child = _IOSDragHandle( + handleHeight: handleHeight, + handleColor: handleColor, + handleWidth: handleWidth, + handleBallWidth: handleBallWidth, + handleType: handleType, + debugPaintSizeEnabled: debugPaintSizeEnabled, + ); + } else if (Platform.isAndroid) { + child = _AndroidDragHandle( + handleHeight: handleHeight, + handleColor: handleColor, + handleWidth: handleWidth, + handleBallWidth: handleBallWidth, + handleType: handleType, + debugPaintSizeEnabled: debugPaintSizeEnabled, + ); + } else { + throw UnsupportedError('Unsupported platform'); + } + + if (debugPaintSizeEnabled) { + child = ColoredBox( + color: Colors.red.withOpacity(0.5), + child: child, + ); + } + + if (handleType != HandleType.none && handleType != HandleType.collapsed) { + final offset = Platform.isIOS ? -handleWidth : 0.0; + child = Stack( + clipBehavior: Clip.none, + children: [ + if (handleType == HandleType.left) + Positioned( + left: offset, + child: child, + ), + if (handleType == HandleType.right) + Positioned( + right: offset, + child: child, + ), + ], + ); + } + + return child; + } +} + +class _IOSDragHandle extends _IDragHandle { + const _IOSDragHandle({ + required super.handleHeight, + super.handleColor, + super.handleWidth, + super.handleBallWidth, + required super.handleType, + super.debugPaintSizeEnabled, + }); + + @override + Widget build(BuildContext context) { + Widget child; + if (handleType == HandleType.collapsed) { + child = Container( + width: handleWidth, + color: handleColor, + height: handleHeight, + ); + } else { + child = Column( + mainAxisSize: MainAxisSize.max, + children: [ + if (handleType == HandleType.left) + Container( + width: handleBallWidth, + height: handleBallWidth, + decoration: BoxDecoration( + color: handleColor, + shape: BoxShape.circle, + ), + ), + if (handleType == HandleType.right) + SizedBox( + width: handleBallWidth, + height: handleBallWidth, + ), + Container( + width: handleWidth, + color: handleColor, + height: handleHeight - 2.0 * handleBallWidth, + ), + if (handleType == HandleType.right) + Container( + width: handleBallWidth, + height: handleBallWidth, + decoration: BoxDecoration( + color: handleColor, + shape: BoxShape.circle, + ), + ), + if (handleType == HandleType.left) + SizedBox( + width: handleBallWidth, + height: handleBallWidth, + ), + ], + ); + } + + final editorState = context.read(); + final ballWidth = handleBallWidth; + + child = GestureDetector( + behavior: HitTestBehavior.opaque, + dragStartBehavior: DragStartBehavior.down, + onPanStart: (details) { + editorState.service.selectionService.onPanStart( + details.translate(0, -ballWidth), + handleType.dragMode, + ); + }, + onPanUpdate: (details) { + editorState.service.selectionService.onPanUpdate( + details.translate(0, -ballWidth), + handleType.dragMode, + ); + }, + onPanEnd: (details) { + editorState.service.selectionService.onPanEnd( + details, + handleType.dragMode, + ); + }, + child: child, + ); + + return child; + } +} + +// ignore: must_be_immutable +class _AndroidDragHandle extends _IDragHandle { + _AndroidDragHandle({ + required super.handleHeight, + super.handleColor, + super.handleWidth, + super.handleBallWidth, + required super.handleType, + super.debugPaintSizeEnabled, + }); + + Selection? selection; + + @override + Widget build(BuildContext context) { + final editorState = context.read(); + Widget child = SizedBox( + width: handleWidth, + height: handleHeight - 2.0 * handleBallWidth, + ); + + if (handleType == HandleType.none) { + return child; + } + + final ballWidth = handleBallWidth * 2.0; + + child = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: handleType.crossAxisAlignment, + children: [ + child, + if (handleType == HandleType.collapsed) + Transform.rotate( + angle: pi / 4.0, + child: Container( + width: ballWidth, + height: ballWidth, + decoration: BoxDecoration( + color: handleColor, + borderRadius: BorderRadius.only( + topRight: Radius.circular(handleBallWidth), + bottomLeft: Radius.circular(handleBallWidth), + bottomRight: Radius.circular(handleBallWidth), + ), + ), + ), + ), + if (handleType == HandleType.left) + Container( + width: ballWidth, + height: ballWidth, + decoration: BoxDecoration( + color: handleColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(handleBallWidth), + bottomLeft: Radius.circular(handleBallWidth), + bottomRight: Radius.circular(handleBallWidth), + ), + ), + ), + if (handleType == HandleType.right) + Container( + width: ballWidth, + height: ballWidth, + decoration: BoxDecoration( + color: handleColor, + borderRadius: BorderRadius.only( + topRight: Radius.circular(handleBallWidth), + bottomLeft: Radius.circular(handleBallWidth), + bottomRight: Radius.circular(handleBallWidth), + ), + ), + ), + ], + ); + + child = GestureDetector( + behavior: HitTestBehavior.opaque, + dragStartBehavior: DragStartBehavior.down, + onPanStart: (details) { + selection = editorState.service.selectionService.onPanStart( + details.translate(0, -ballWidth), + handleType.dragMode, + ); + }, + onPanUpdate: (details) { + final selection = editorState.service.selectionService.onPanUpdate( + details.translate(0, -ballWidth), + handleType.dragMode, + ); + if (this.selection != selection) { + HapticFeedback.selectionClick(); + } + this.selection = selection; + }, + onPanEnd: (details) { + editorState.service.selectionService.onPanEnd( + details, + handleType.dragMode, + ); + }, + child: child, + ); + + return child; + } +} + +extension on DragStartDetails { + DragStartDetails translate(double dx, double dy) { + return DragStartDetails( + sourceTimeStamp: sourceTimeStamp, + globalPosition: Offset(globalPosition.dx + dx, globalPosition.dy + dy), + localPosition: Offset(localPosition.dx + dx, localPosition.dy + dy), + ); + } +} + +extension on DragUpdateDetails { + DragUpdateDetails translate(double dx, double dy) { + return DragUpdateDetails( + sourceTimeStamp: sourceTimeStamp, + globalPosition: Offset(globalPosition.dx + dx, globalPosition.dy + dy), + localPosition: Offset(localPosition.dx + dx, localPosition.dy + dy), + delta: Offset(delta.dx + dx, delta.dy + dy), + primaryDelta: primaryDelta, + ); + } +} diff --git a/lib/src/render/selection/mobile_collapsed_handle.dart b/lib/src/render/selection/mobile_collapsed_handle.dart new file mode 100644 index 000000000..1b0374bf4 --- /dev/null +++ b/lib/src/render/selection/mobile_collapsed_handle.dart @@ -0,0 +1,143 @@ +import 'dart:io'; + +import 'package:appflowy_editor/src/render/selection/mobile_basic_handle.dart'; +import 'package:flutter/material.dart'; + +class MobileCollapsedHandle extends StatelessWidget { + const MobileCollapsedHandle({ + super.key, + required this.layerLink, + required this.rect, + this.handleColor = Colors.black, + this.handleBallWidth = 6.0, + this.handleWidth = 2.0, + this.enableHapticFeedbackOnAndroid = true, + }); + + final Rect rect; + final LayerLink layerLink; + final Color handleColor; + final double handleWidth; + final double handleBallWidth; + final bool enableHapticFeedbackOnAndroid; + + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return _IOSCollapsedHandle( + layerLink: layerLink, + rect: rect, + handleWidth: handleWidth, + ); + } else if (Platform.isAndroid) { + return _AndroidCollapsedHandle( + layerLink: layerLink, + rect: rect, + handleColor: handleColor, + handleWidth: handleWidth, + handleBallWidth: handleBallWidth, + enableHapticFeedbackOnAndroid: enableHapticFeedbackOnAndroid, + ); + } + throw UnsupportedError('Unsupported platform'); + } +} + +class _IOSCollapsedHandle extends StatelessWidget { + const _IOSCollapsedHandle({ + required this.layerLink, + required this.rect, + this.handleWidth = 2.0, + }); + + final Rect rect; + final LayerLink layerLink; + final double handleWidth; + + @override + Widget build(BuildContext context) { + // Extend the click area to make it easier to click. + const extend = 10.0; + final adjustedRect = Rect.fromLTWH( + rect.left - extend, + rect.top - extend, + rect.width + 2 * extend, + rect.height + 2 * extend, + ); + return Positioned.fromRect( + rect: adjustedRect, + child: CompositedTransformFollower( + link: layerLink, + offset: adjustedRect.topLeft, + showWhenUnlinked: false, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + child: DragHandle( + handleHeight: adjustedRect.height, + handleType: HandleType.collapsed, + handleColor: Colors.transparent, + handleWidth: adjustedRect.width, + ), + ), + ], + ), + ), + ); + } +} + +class _AndroidCollapsedHandle extends StatelessWidget { + const _AndroidCollapsedHandle({ + required this.layerLink, + required this.rect, + this.handleColor = Colors.black, + this.handleBallWidth = 6.0, + this.handleWidth = 2.0, + this.enableHapticFeedbackOnAndroid = true, + }); + + final Rect rect; + final LayerLink layerLink; + final Color handleColor; + final double handleWidth; + final double handleBallWidth; + final bool enableHapticFeedbackOnAndroid; + + @override + Widget build(BuildContext context) { + // Extend the click area to make it easier to click. + final adjustedRect = Rect.fromLTWH( + rect.left - 2 * (handleBallWidth), + rect.top, + rect.width + 4 * (handleBallWidth), + // Enable clicking in the handle area outside the stack. + // https://github.com/flutter/flutter/issues/75747 + rect.height + 2 * handleBallWidth, + ); + return Positioned.fromRect( + rect: adjustedRect, + child: CompositedTransformFollower( + link: layerLink, + offset: adjustedRect.topLeft, + showWhenUnlinked: false, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + top: 4.0, + child: DragHandle( + handleHeight: adjustedRect.height, + handleType: HandleType.collapsed, + handleColor: handleColor, + handleWidth: adjustedRect.width, + handleBallWidth: handleBallWidth, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/render/selection/mobile_selection_handle.dart b/lib/src/render/selection/mobile_selection_handle.dart new file mode 100644 index 000000000..5b0fa3b3e --- /dev/null +++ b/lib/src/render/selection/mobile_selection_handle.dart @@ -0,0 +1,72 @@ +import 'dart:io'; + +import 'package:appflowy_editor/src/render/selection/mobile_basic_handle.dart'; +import 'package:flutter/material.dart'; + +class MobileSelectionHandle extends StatelessWidget { + const MobileSelectionHandle({ + super.key, + required this.layerLink, + required this.rect, + this.handleType = HandleType.none, + this.handleColor = Colors.black, + this.handleBallWidth = 6.0, + this.handleWidth = 2.0, + this.enableHapticFeedbackOnAndroid = true, + }); + + final Rect rect; + final LayerLink layerLink; + final HandleType handleType; + final Color handleColor; + final double handleWidth; + final double handleBallWidth; + final bool enableHapticFeedbackOnAndroid; + + @override + Widget build(BuildContext context) { + assert(handleType != HandleType.collapsed); + + var adjustedRect = rect; + if (handleType != HandleType.none) { + if (Platform.isIOS) { + // on iOS, the cursor will still be visible if the selection is not collapsed. + // So, adding a threshold padding to avoid row overflow. + const threshold = 0.25; + adjustedRect = Rect.fromLTWH( + rect.left - 2 * (handleWidth + threshold), + rect.top - handleBallWidth, + rect.width + 4 * (handleWidth + threshold), + rect.height + 2 * handleBallWidth, + ); + } else if (Platform.isAndroid) { + // on Android, normally the cursor will be hidden if the selection is not collapsed. + // Extend the click area to make it easier to click. + adjustedRect = Rect.fromLTWH( + rect.left - 2 * (handleBallWidth), + rect.top, + rect.width + 4 * (handleBallWidth), + // Enable clicking in the handle area outside the stack. + // https://github.com/flutter/flutter/issues/75747 + rect.height + 2 * handleBallWidth, + ); + } + } + + return Positioned.fromRect( + rect: adjustedRect, + child: CompositedTransformFollower( + link: layerLink, + offset: adjustedRect.topLeft, + showWhenUnlinked: false, + child: DragHandle( + handleType: handleType, + handleColor: handleColor, + handleHeight: adjustedRect.height, + handleWidth: handleWidth, + handleBallWidth: handleBallWidth, + ), + ), + ); + } +} diff --git a/lib/src/render/selection/mobile_selection_widget.dart b/lib/src/render/selection/mobile_selection_widget.dart deleted file mode 100644 index 81a25e869..000000000 --- a/lib/src/render/selection/mobile_selection_widget.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:flutter/material.dart'; - -class MobileSelectionWidget extends StatelessWidget { - const MobileSelectionWidget({ - super.key, - required this.layerLink, - required this.rect, - required this.color, - this.decoration, - this.showLeftHandler = false, - this.showRightHandler = false, - this.handlerColor = Colors.black, - this.handlerBallWidth = 6.0, - this.handlerWidth = 2.0, - }); - - final Color color; - final Rect rect; - final LayerLink layerLink; - final BoxDecoration? decoration; - final bool showLeftHandler; - final bool showRightHandler; - final Color handlerColor; - final double handlerWidth; - final double handlerBallWidth; - - @override - Widget build(BuildContext context) { - // to avoid row overflow - const threshold = 0.25; - // left and right add 2px to avoid the selection area from being too narrow - var adjustedRect = rect; - if (showLeftHandler || showRightHandler) { - adjustedRect = Rect.fromLTWH( - rect.left - 2 * (handlerWidth + threshold), - rect.top - handlerBallWidth, - rect.width + 4 * (handlerWidth + threshold), - rect.height + 2 * handlerBallWidth, - ); - } - return Positioned.fromRect( - rect: adjustedRect, - child: CompositedTransformFollower( - link: layerLink, - offset: adjustedRect.topLeft, - showWhenUnlinked: false, - // Ignore the gestures in selection overlays - // to solve the problem that selection areas cannot overlap. - child: IgnorePointer( - child: MobileSelectionWithHandler( - color: color, - decoration: decoration, - showLeftHandler: showLeftHandler, - showRightHandler: showRightHandler, - handlerColor: handlerColor, - handlerHeight: adjustedRect.height, - handlerWidth: handlerWidth, - handlerBallWidth: handlerBallWidth, - ), - ), - ), - ); - } -} - -class MobileSelectionWithHandler extends StatelessWidget { - const MobileSelectionWithHandler({ - super.key, - required this.color, - this.showLeftHandler = false, - this.showRightHandler = false, - this.handlerColor = Colors.black, - this.decoration, - this.handlerWidth = 2.0, - required this.handlerHeight, - required this.handlerBallWidth, - }); - - final Color color; - final BoxDecoration? decoration; - - final bool showLeftHandler; - final bool showRightHandler; - final Color handlerColor; - final double handlerWidth; - final double handlerHeight; - final double handlerBallWidth; - - @override - Widget build(BuildContext context) { - Widget child = Container( - color: decoration == null ? color : null, - decoration: decoration, - ); - if (showLeftHandler || showRightHandler) { - child = Stack( - clipBehavior: Clip.none, - children: [ - if (showLeftHandler) - Positioned( - left: -handlerWidth, - child: _DragHandler( - handlerColor: handlerColor, - handlerWidth: handlerWidth, - handlerBallWidth: handlerBallWidth, - handlerHeight: handlerHeight, - showLeftHandler: true, - showRightHandler: false, - ), - ), - child, - if (showRightHandler) - Positioned( - right: -handlerWidth, - child: _DragHandler( - handlerColor: handlerColor, - handlerWidth: handlerWidth, - handlerBallWidth: handlerBallWidth, - handlerHeight: handlerHeight, - showRightHandler: true, - showLeftHandler: false, - ), - ), - ], - ); - } - return child; - } -} - -class _DragHandler extends StatelessWidget { - const _DragHandler({ - required this.handlerHeight, - this.handlerColor = Colors.black, - this.handlerWidth = 2.0, - this.handlerBallWidth = 6.0, - this.showLeftHandler = false, - this.showRightHandler = false, - }); - - final Color handlerColor; - final double handlerWidth; - final double handlerHeight; - final double handlerBallWidth; - final bool showLeftHandler; - final bool showRightHandler; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.max, - children: [ - if (showLeftHandler) - Container( - width: handlerBallWidth, - height: handlerBallWidth, - decoration: BoxDecoration( - color: handlerColor, - shape: BoxShape.circle, - ), - ), - if (showRightHandler) - SizedBox( - width: handlerBallWidth, - height: handlerBallWidth, - ), - Container( - width: handlerWidth, - color: handlerColor, - height: handlerHeight - 2.0 * handlerBallWidth, - ), - if (showRightHandler) - Container( - width: handlerBallWidth, - height: handlerBallWidth, - decoration: BoxDecoration( - color: handlerColor, - shape: BoxShape.circle, - ), - ), - if (showLeftHandler) - SizedBox( - width: handlerBallWidth, - height: handlerBallWidth, - ), - ], - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index b3e861aa4..7305b2a29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appflowy_editor description: A highly customizable rich-text editor for Flutter. The AppFlowy Editor project for AppFlowy and beyond. -version: 2.1.0 +version: 2.2.0 homepage: https://github.com/AppFlowy-IO/appflowy-editor topics: @@ -48,6 +48,7 @@ dependencies: universal_html: ^2.2.4 keyboard_height_plugin: ^0.0.4 numerus: ^2.1.2 + device_info_plus: ^9.1.1 dev_dependencies: flutter_test: diff --git a/test/render/selection/mobile_selection_widget_test.dart b/test/render/selection/mobile_selection_widget_test.dart deleted file mode 100644 index 754f0602e..000000000 --- a/test/render/selection/mobile_selection_widget_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:appflowy_editor/src/core/document/node.dart'; -import 'package:appflowy_editor/src/render/selection/mobile_selection_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../test_helper.dart'; - -void main() { - group('MobileSelectionWidget', () { - testWidgets('can render', (tester) async { - final node = Node(type: 'paragraph'); - - await tester.buildAndPump( - Stack( - children: [ - MobileSelectionWidget( - showLeftHandler: true, - showRightHandler: true, - layerLink: node.layerLink, - rect: const Rect.fromLTWH(0, 0, 100, 100), - color: Colors.red, - ), - ], - ), - ); - await tester.pumpAndSettle(); - - expect(find.byType(MobileSelectionWidget), findsOneWidget); - }); - }); -}