Merge pull request #768 from LucasXu0/feat/auto_scroll_service

implement autoscrolling on edge touch
This commit is contained in:
Nathan.fooo 2022-08-05 19:25:02 +08:00 committed by GitHub
commit d758cc749a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 866 additions and 230 deletions

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 4L12.5 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 8H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 12H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="4" r="0.5" fill="#333333"/>
<circle cx="4" cy="8" r="0.5" fill="#333333"/>
<circle cx="4" cy="12" r="0.5" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.25344 3.59998H9.63344V12H8.25344V8.36398H4.65344V12H3.27344V3.59998H4.65344V7.04398H8.25344V3.59998Z" fill="#333333"/>
<path d="M12.0325 6.39998H12.9925V12H11.8885V7.56798L10.8325 7.86398L10.5605 6.91998L12.0325 6.39998Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.67531 3.59998H9.05531V12H7.67531V8.36398H4.07531V12H2.69531V3.59998H4.07531V7.04398H7.67531V3.59998Z" fill="#333333"/>
<path d="M10.1104 12V11.176L12.0224 9.20798C12.449 8.75998 12.6624 8.38664 12.6624 8.08798C12.6624 7.86931 12.593 7.69331 12.4544 7.55998C12.321 7.42664 12.1477 7.35998 11.9344 7.35998C11.513 7.35998 11.201 7.57864 10.9984 8.01598L10.0704 7.47198C10.2464 7.08798 10.4997 6.79464 10.8304 6.59198C11.161 6.38931 11.5237 6.28798 11.9184 6.28798C12.425 6.28798 12.8597 6.44798 13.2224 6.76798C13.585 7.08264 13.7664 7.50931 13.7664 8.04798C13.7664 8.62931 13.4597 9.22664 12.8464 9.83998L11.7504 10.936H13.8544V12H10.1104Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.62063 3.59998H9.00063V12H7.62063V8.36398H4.02062V12H2.64062V3.59998H4.02062V7.04398H7.62063V3.59998Z" fill="#333333"/>
<path d="M12.6637 8.67198C13.0424 8.78398 13.349 8.98131 13.5837 9.26398C13.8237 9.54131 13.9437 9.87731 13.9437 10.272C13.9437 10.848 13.749 11.2986 13.3597 11.624C12.9757 11.9493 12.5037 12.112 11.9437 12.112C11.5064 12.112 11.1144 12.0133 10.7677 11.816C10.4264 11.6133 10.1784 11.3173 10.0237 10.928L10.9677 10.384C11.1064 10.816 11.4317 11.032 11.9437 11.032C12.2264 11.032 12.445 10.9653 12.5997 10.832C12.7597 10.6933 12.8397 10.5066 12.8397 10.272C12.8397 10.0426 12.7597 9.85864 12.5997 9.71998C12.445 9.58131 12.2264 9.51198 11.9437 9.51198H11.7037L11.2797 8.87198L12.3837 7.43198H10.1917V6.39998H13.7117V7.31198L12.6637 8.67198Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 892 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z" fill="#333333"/>
<path d="M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4L14 8L10 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 4L2 8L6 12" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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. AppFlowys 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."
}
]
}
]
}

View File

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:convert';
import 'package:example/expandable_floating_action_button.dart';
@ -80,12 +81,21 @@ class _MyHomePageState extends State<MyHomePage> {
icon: const Icon(Icons.note_add),
),
ActionButton(
icon: const Icon(Icons.document_scanner),
onPressed: () {
if (page == 1) return;
setState(() {
page = 1;
});
},
),
ActionButton(
onPressed: () {
if (page == 2) return;
setState(() {
page = 2;
});
},
icon: const Icon(Icons.text_fields),
),
],
@ -97,11 +107,41 @@ class _MyHomePageState extends State<MyHomePage> {
if (page == 0) {
return _buildFlowyEditor();
} else if (page == 1) {
return _buildFlowyEditorWithEmptyDocument();
} else if (page == 2) {
return _buildTextField();
}
return Container();
}
Widget _buildFlowyEditorWithEmptyDocument() {
return Container(
padding: const EdgeInsets.only(left: 20, right: 20),
child: FlowyEditor(
key: editorKey,
editorState: EditorState(
document: StateTree(
root: Node(
type: 'editor',
children: LinkedList()
..add(
TextNode.empty()
..delta = Delta(
[TextInsert('')],
),
),
attributes: {},
),
),
),
keyEventHandlers: const [],
customBuilders: {
'image': ImageNodeBuilder(),
},
),
);
}
Widget _buildFlowyEditor() {
return FutureBuilder<String>(
future: rootBundle.loadString('assets/example.json'),

View File

@ -83,6 +83,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "5.4.3+7"
flutter_lints:
dependency: "direct dev"
description:

View File

@ -38,6 +38,7 @@ dependencies:
path: ../
provider: ^6.0.3
url_launcher: ^6.1.5
flutter_inappwebview: ^5.4.3+7
dev_dependencies:
flutter_test:

View File

@ -60,10 +60,8 @@ class EditorState {
for (final op in transaction.operations) {
_applyOperation(op);
}
// updateCursorSelection(transaction.afterSelection);
// FIXME: don't use delay
Future.delayed(const Duration(milliseconds: 16), () {
WidgetsBinding.instance.addPostFrameCallback((_) {
updateCursorSelection(transaction.afterSelection);
});

View File

@ -9,6 +9,7 @@ extension NodeExtensions on Node {
RenderBox? get renderBox =>
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
BuildContext? get context => key?.currentContext;
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
bool inSelection(Selection selection) {

View File

@ -18,20 +18,20 @@ class FlowySvg extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (name != null) {
return SizedBox.fromSize(
size: size,
child: SvgPicture.asset(
'assets/images/$name.svg',
color: color,
package: 'flowy_editor',
),
return SvgPicture.asset(
'assets/images/$name.svg',
color: color,
package: 'flowy_editor',
width: size.width,
height: size.width,
);
} else if (number != null) {
final numberText =
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
return SizedBox.fromSize(
size: size,
child: SvgPicture.string(numberText),
return SvgPicture.string(
numberText,
width: size.width,
height: size.width,
);
}
return Container();

View File

@ -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<Node> {
@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<TextNode>(
context: context,
node: child,
editorState: editorState,
)
: NodeWidgetContext<Node>(
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<TextNode>(
context: context,
node: child,
editorState: editorState,
)
: NodeWidgetContext<Node>(
context: context,
node: child,
editorState: editorState,
),
),
)
.toList(),
);
}
}

View File

@ -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<TextNode> {
@override
Widget build(NodeWidgetContext<TextNode> context) {
@ -129,7 +129,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
}
Widget _buildRichText(BuildContext context) {
return _buildSingleRichText(context);
return Align(
alignment: Alignment.centerLeft,
child: _buildSingleRichText(context),
);
}
Widget _buildSingleRichText(BuildContext context) {
@ -170,11 +173,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
}
TextSpan get _textSpan => TextSpan(
children: widget.textNode.delta.operations
.whereType<TextInsert>()
.map((insert) => RichTextStyle(
attributes: insert.attributes ?? {},
text: insert.content,
).toTextSpan())
.toList(growable: false));
children: widget.textNode.delta.operations
.whereType<TextInsert>()
.map((insert) => RichTextStyle(
attributes: insert.attributes ?? {},
text: insert.content,
).toTextSpan())
.toList(growable: false),
);
}

View File

@ -201,6 +201,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
),
);
});
// TODO: disable scrolling.
Overlay.of(context)?.insert(_listToolbarOverlay!);
}

View File

@ -1,5 +1,7 @@
import 'package:flowy_editor/document/attributes.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/extensions/text_node_extensions.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
@ -46,13 +48,20 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
final builder = TransactionBuilder(editorState);
for (final textNode in textNodes) {
builder.updateNode(
textNode,
Attributes.fromIterable(
StyleKey.globalStyleKeys,
value: (_) => null,
)..addAll(attributes),
);
builder
..updateNode(
textNode,
Attributes.fromIterable(
StyleKey.globalStyleKeys,
value: (_) => null,
)..addAll(attributes),
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: textNode.toRawString().length,
),
);
}
builder.commit();

View File

@ -1,5 +1,3 @@
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/material.dart';
import 'package:flowy_editor/editor_state.dart';
@ -7,18 +5,21 @@ import 'package:flowy_editor/render/editor/editor_entry.dart';
import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart';
import 'package:flowy_editor/render/rich_text/checkbox_text.dart';
import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
import 'package:flowy_editor/service/input_service.dart';
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
import 'package:flowy_editor/service/render_plugin_service.dart';
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart';
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart';
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/service/selection_service.dart';
import 'package:flowy_editor/render/rich_text/heading_text.dart';
import 'package:flowy_editor/render/rich_text/number_list_text.dart';
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/copy_paste_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';
NodeWidgetBuilders defaultBuilders = {
@ -62,6 +63,8 @@ class FlowyEditor extends StatefulWidget {
}
class _FlowyEditorState extends State<FlowyEditor> {
late ScrollController _scrollController;
EditorState get editorState => widget.editorState;
@override
@ -71,6 +74,13 @@ class _FlowyEditorState extends State<FlowyEditor> {
editorState.service.renderPluginService = _createRenderPlugin();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant FlowyEditor oldWidget) {
super.didUpdateWidget(oldWidget);
@ -82,33 +92,36 @@ class _FlowyEditorState extends State<FlowyEditor> {
@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(

View File

@ -1,12 +0,0 @@
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// type '/' to trigger shortcut widget
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
if (event.logicalKey != LogicalKeyboardKey.slash) {
return KeyEventResult.ignored;
}
return KeyEventResult.ignored;
};

View File

@ -0,0 +1,300 @@
import 'dart:math';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/infra/flowy_svg.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart';
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
final List<PopupListItem> _popupListItems = [
PopupListItem(
text: 'Text',
icon: _popupListIcon('text'),
handler: (editorState) => formatText(editorState),
),
PopupListItem(
text: 'Heading 1',
icon: _popupListIcon('h1'),
handler: (editorState) => formatHeading(editorState, StyleKey.h1),
),
PopupListItem(
text: 'Heading 2',
icon: _popupListIcon('h2'),
handler: (editorState) => formatHeading(editorState, StyleKey.h2),
),
PopupListItem(
text: 'Heading 3',
icon: _popupListIcon('h3'),
handler: (editorState) => formatHeading(editorState, StyleKey.h3),
),
PopupListItem(
text: 'Bullets',
icon: _popupListIcon('bullets'),
handler: (editorState) => formatBulletedList(editorState),
),
PopupListItem(
text: 'Numbered list',
icon: _popupListIcon('number'),
handler: (editorState) => debugPrint('Not implement yet!'),
),
PopupListItem(
text: 'Checkboxes',
icon: _popupListIcon('checkbox'),
handler: (editorState) => formatCheckbox(editorState),
),
];
OverlayEntry? _popupListOverlay;
EditorState? _editorState;
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
if (event.logicalKey != LogicalKeyboardKey.slash) {
return KeyEventResult.ignored;
}
final textNodes = editorState
.service.selectionService.currentSelectedNodes.value
.whereType<TextNode>();
if (textNodes.length != 1) {
return KeyEventResult.ignored;
}
final selection = editorState.service.selectionService.currentSelection;
final textNode = textNodes.first;
final context = textNode.context;
final selectable = textNode.selectable;
if (selection == null || context == null || selectable == null) {
return KeyEventResult.ignored;
}
final rect = selectable.getCursorRectInPosition(selection.start);
final offset = selectable.localToGlobal(rect.topLeft);
if (!selection.isCollapsed) {
TransactionBuilder(editorState)
..deleteText(
textNode,
selection.start.offset,
selection.end.offset - selection.start.offset,
)
..commit();
}
_popupListOverlay?.remove();
_popupListOverlay = OverlayEntry(
builder: (context) => Positioned(
top: offset.dy + 15.0,
left: offset.dx + 5.0,
child: PopupListWidget(
editorState: editorState,
items: _popupListItems,
),
),
);
Overlay.of(context)?.insert(_popupListOverlay!);
editorState.service.selectionService.currentSelectedNodes
.removeListener(clearPopupListOverlay);
editorState.service.selectionService.currentSelectedNodes
.addListener(clearPopupListOverlay);
// editorState.service.keyboardService?.disable();
_editorState = editorState;
return KeyEventResult.handled;
};
void clearPopupListOverlay() {
_popupListOverlay?.remove();
_popupListOverlay = null;
_editorState?.service.keyboardService?.enable();
_editorState = null;
}
class PopupListWidget extends StatefulWidget {
const PopupListWidget({
Key? key,
required this.editorState,
required this.items,
this.maxItemInRow = 5,
}) : super(key: key);
final EditorState editorState;
final List<PopupListItem> items;
final int maxItemInRow;
@override
State<PopupListWidget> createState() => _PopupListWidgetState();
}
class _PopupListWidgetState extends State<PopupListWidget> {
final focusNode = FocusNode(debugLabel: 'popup_list_widget');
var selectedIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: focusNode,
onKey: _onKey,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
),
],
borderRadius: BorderRadius.circular(6.0),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumns(widget.items, selectedIndex),
),
),
);
}
List<Widget> _buildColumns(List<PopupListItem> items, int selectedIndex) {
List<Widget> columns = [];
List<Widget> itemWidgets = [];
for (var i = 0; i < items.length; i++) {
if (i != 0 && i % (widget.maxItemInRow) == 0) {
columns.add(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: itemWidgets,
));
itemWidgets = [];
}
itemWidgets.add(_PopupListItemWidget(
editorState: widget.editorState,
item: items[i],
highlight: selectedIndex == i,
));
}
if (itemWidgets.isNotEmpty) {
columns.add(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: itemWidgets,
));
itemWidgets = [];
}
return columns;
}
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.enter) {
widget.items[selectedIndex].handler(widget.editorState);
return KeyEventResult.handled;
}
var newSelectedIndex = selectedIndex;
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
newSelectedIndex -= widget.maxItemInRow;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
newSelectedIndex += widget.maxItemInRow;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
newSelectedIndex -= 1;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
newSelectedIndex += 1;
}
if (newSelectedIndex != selectedIndex) {
setState(() {
selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex));
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
}
class _PopupListItemWidget extends StatelessWidget {
const _PopupListItemWidget({
Key? key,
required this.highlight,
required this.item,
required this.editorState,
}) : super(key: key);
final EditorState editorState;
final PopupListItem item;
final bool highlight;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
child: SizedBox(
width: 140,
child: TextButton.icon(
icon: item.icon,
style: ButtonStyle(
alignment: Alignment.centerLeft,
overlayColor: MaterialStateProperty.all(
const Color(0xFFE0F8FF),
),
backgroundColor: highlight
? MaterialStateProperty.all(const Color(0xFFE0F8FF))
: MaterialStateProperty.all(Colors.transparent),
),
label: Text(
item.text,
textAlign: TextAlign.left,
style: const TextStyle(
color: Colors.black,
fontSize: 14.0,
),
),
onPressed: () {
item.handler(editorState);
},
),
),
);
}
}
class PopupListItem {
PopupListItem({
required this.text,
this.message = '',
required this.icon,
required this.handler,
});
final String text;
final String message;
final Widget icon;
final void Function(EditorState editorState) handler;
}
Widget _popupListIcon(String name) => FlowySvg(
name: 'popup_list/$name',
color: Colors.black,
size: const Size.square(18.0),
);

View File

@ -3,6 +3,11 @@ import 'package:flutter/services.dart';
import '../editor_state.dart';
import 'package:flutter/material.dart';
mixin FlowyKeyboardService<T extends StatefulWidget> on State<T> {
void enable();
void disable();
}
typedef FlowyKeyEventHandler = KeyEventResult Function(
EditorState editorState,
RawKeyEvent event,
@ -25,9 +30,12 @@ class FlowyKeyboard extends StatefulWidget {
State<FlowyKeyboard> createState() => _FlowyKeyboardState();
}
class _FlowyKeyboardState extends State<FlowyKeyboard> {
class _FlowyKeyboardState extends State<FlowyKeyboard>
with FlowyKeyboardService {
final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
bool isFocus = true;
@override
Widget build(BuildContext context) {
return Focus(
@ -38,7 +46,30 @@ class _FlowyKeyboardState extends State<FlowyKeyboard> {
);
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
void enable() {
isFocus = true;
focusNode.requestFocus();
}
@override
void disable() {
isFocus = false;
focusNode.unfocus();
}
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
if (!isFocus) {
return KeyEventResult.ignored;
}
debugPrint('on keyboard event $event');
if (event is! RawKeyDownEvent) {

View File

@ -0,0 +1,65 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
mixin FlowyScrollService<T extends StatefulWidget> on State<T> {
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<FlowyScroll> createState() => _FlowyScrollState();
}
class _FlowyScrollState extends State<FlowyScroll> 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();
}
}

View File

@ -1,13 +1,14 @@
import 'dart:async';
import 'package:flowy_editor/document/node_iterator.dart';
import 'package:flowy_editor/document/state_tree.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/node_iterator.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/document/state_tree.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flowy_editor/render/selection/cursor_widget.dart';
@ -101,102 +102,18 @@ class FlowySelection extends StatefulWidget {
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>
with FlowySelectionService, WidgetsBindingObserver {
final _cursorKey = GlobalKey(debugLabel: 'cursor');
final List<OverlayEntry> _selectionOverlays = [];
final List<OverlayEntry> _cursorOverlays = [];
OverlayEntry? _debugOverlay;
/// [Pan] and [Tap] must be mutually exclusive.
/// Pan
Offset? panStartOffset;
double? panStartScrollDy;
Offset? panEndOffset;
/// Tap
@ -261,7 +178,7 @@ class _FlowySelectionState extends State<FlowySelection>
@override
void updateSelection(Selection selection) {
_rects.clear();
_clearSelection();
clearSelection();
// cursor
if (selection.isCollapsed) {
@ -275,7 +192,19 @@ class _FlowySelectionState extends State<FlowySelection>
@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
@ -327,7 +256,7 @@ class _FlowySelectionState extends State<FlowySelection>
}
}
for (final child in node.children) {
result.addAll(computeNodesInRange(child, start, end));
result.addAll(_computeNodesInRange(child, start, end));
}
return result;
}
@ -413,12 +342,24 @@ class _FlowySelectionState extends State<FlowySelection>
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;
}
@ -429,40 +370,30 @@ class _FlowySelectionState extends State<FlowySelection>
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.updateCursorSelection(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, selection);
@ -555,12 +486,12 @@ class _FlowySelectionState extends State<FlowySelection>
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);
@ -579,4 +510,139 @@ class _FlowySelectionState extends State<FlowySelection>
final endNode = stateTree.nodeAtPath(selection.end.path)!;
return NodeIterator(stateTree, startNode, endNode).toList();
}
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>(
() => 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();
}
}

View File

@ -1,8 +1,11 @@
import 'package:flowy_editor/service/render_plugin_service.dart';
import 'package:flowy_editor/service/toolbar_service.dart';
import 'package:flowy_editor/service/selection_service.dart';
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/scroll_service.dart';
import 'package:flowy_editor/service/selection_service.dart';
import 'package:flowy_editor/service/toolbar_service.dart';
class FlowyService {
// selection service
final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service');
@ -14,6 +17,13 @@ class FlowyService {
// keyboard service
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
FlowyKeyboardService? get keyboardService {
if (keyboardServiceKey.currentState != null &&
keyboardServiceKey.currentState is FlowyKeyboardService) {
return keyboardServiceKey.currentState! as FlowyKeyboardService;
}
return null;
}
// input service
final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');
@ -23,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;
}

View File

@ -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<FlowyToolbar> createState() => _FlowyToolbarState();
}
class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService {
class _FlowyToolbarState extends State<FlowyToolbar> with FlowyToolbarService {
OverlayEntry? _toolbarOverlay;
@override

View File

@ -29,6 +29,7 @@ flutter:
# To add assets to your package, add an assets section, like this:
assets:
- assets/images/toolbar/
- assets/images/popup_list/
- assets/images/
- assets/document.json
# - images/a_dot_burr.jpeg