diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg new file mode 100644 index 0000000000..85640695af --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg new file mode 100644 index 0000000000..c2c962fa0b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/bulleted_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg new file mode 100644 index 0000000000..3e57a6b000 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg new file mode 100644 index 0000000000..6b739a761f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg new file mode 100644 index 0000000000..2db0ab3b64 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/number_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg new file mode 100644 index 0000000000..8e55d9e2e3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg new file mode 100644 index 0000000000..b37bb9acc0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/strikethrough.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg new file mode 100644 index 0000000000..933471e6a7 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/assets/images/toolbar/underline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index a2fcc7a4df..158e33bbb1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -123,32 +123,32 @@ class _MyHomePageState extends State { customBuilders: { 'image': ImageNodeBuilder(), }, - shortcuts: [ - // TODO: this won't work, just a example for now. - { - 'h1': (editorState, eventName) { - debugPrint('shortcut => $eventName'); - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.isEmpty) { - return; - } - final textNode = selectedNodes.first as TextNode; - TransactionBuilder(editorState) - ..formatText(textNode, 0, textNode.toRawString().length, { - 'heading': 'h1', - }) - ..commit(); - } - }, - { - 'bold': (editorState, eventName) => - debugPrint('shortcut => $eventName') - }, - { - 'underline': (editorState, eventName) => - debugPrint('shortcut => $eventName') - }, - ], + // shortcuts: [ + // // TODO: this won't work, just a example for now. + // { + // 'h1': (editorState, eventName) { + // debugPrint('shortcut => $eventName'); + // final selectedNodes = editorState.selectedNodes; + // if (selectedNodes.isEmpty) { + // return; + // } + // final textNode = selectedNodes.first as TextNode; + // TransactionBuilder(editorState) + // ..formatText(textNode, 0, textNode.toRawString().length, { + // 'heading': 'h1', + // }) + // ..commit(); + // } + // }, + // { + // 'bold': (editorState, eventName) => + // debugPrint('shortcut => $eventName') + // }, + // { + // 'underline': (editorState, eventName) => + // debugPrint('shortcut => $eventName') + // }, + // ], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart deleted file mode 100644 index 9fbbbbcb01..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/material.dart'; - -typedef FloatingShortcutHandler = void Function( - EditorState editorState, String eventName); -typedef FloatingShortcuts = List>; - -class FloatingShortcutWidget extends StatelessWidget { - const FloatingShortcutWidget({ - Key? key, - required this.editorState, - required this.layerLink, - required this.rect, - required this.floatingShortcuts, - }) : super(key: key); - - final EditorState editorState; - final LayerLink layerLink; - final Rect rect; - final FloatingShortcuts floatingShortcuts; - - List get _shortcutNames => - floatingShortcuts.map((shortcut) => shortcut.keys.first).toList(); - List get _shortcutHandlers => - floatingShortcuts.map((shortcut) => shortcut.values.first).toList(); - - @override - Widget build(BuildContext context) { - return Positioned.fromRect( - rect: rect, - child: CompositedTransformFollower( - link: layerLink, - offset: rect.topLeft, - showWhenUnlinked: true, - child: Container( - color: Colors.white, - child: ListView.builder( - itemCount: floatingShortcuts.length, - itemBuilder: ((context, index) { - final name = _shortcutNameInIndex(index); - final handler = _shortcutHandlerInIndex(index); - return Card( - child: GestureDetector( - onTap: () => handler(editorState, name), - child: ListTile(title: Text(name)), - ), - ); - }), - ), - ), - ), - ); - } - - String _shortcutNameInIndex(int index) => _shortcutNames[index]; - FloatingShortcutHandler _shortcutHandlerInIndex(int index) => - _shortcutHandlers[index]; -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart new file mode 100644 index 0000000000..7266929962 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -0,0 +1,217 @@ +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flutter/material.dart'; + +typedef ToolbarEventHandler = void Function( + EditorState editorState, String eventName); + +typedef ToolbarEventHandlers = List>; +ToolbarEventHandlers defaultToolbarEventHandlers = [ + { + 'bold': ((editorState, eventName) {}), + 'italic': ((editorState, eventName) {}), + 'strikethrough': ((editorState, eventName) {}), + 'underline': ((editorState, eventName) {}), + 'quote': ((editorState, eventName) {}), + 'number_list': ((editorState, eventName) {}), + 'bulleted_list': ((editorState, eventName) {}), + } +]; + +ToolbarEventHandlers defaultListToolbarEventHandlers = [ + { + 'h1': ((editorState, eventName) {}), + }, + { + 'h2': ((editorState, eventName) {}), + }, + { + 'h3': ((editorState, eventName) {}), + }, + { + 'bulleted_list': ((editorState, eventName) {}), + }, + { + 'quote': ((editorState, eventName) {}), + } +]; + +class ToolbarWidget extends StatefulWidget { + ToolbarWidget({ + Key? key, + required this.editorState, + required this.layerLink, + required this.offset, + required this.handlers, + }) : super(key: key); + + final EditorState editorState; + final LayerLink layerLink; + final Offset offset; + final ToolbarEventHandlers handlers; + + @override + State createState() => _ToolbarWidgetState(); +} + +class _ToolbarWidgetState extends State { + final GlobalKey _listToolbarKey = GlobalKey(); + + final toolbarHeight = 32.0; + final topPadding = 5.0; + + final listToolbarWidth = 60.0; + final listToolbarHeight = 120.0; + + final cornerRadius = 8.0; + + OverlayEntry? _listToolbarOverlay; + + @override + void initState() { + super.initState(); + + widget.editorState.service.selectionService.currentSelectedNodes + .addListener(_onSelectionChange); + } + + @override + void dispose() { + widget.editorState.service.selectionService.currentSelectedNodes + .removeListener(_onSelectionChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned( + top: widget.offset.dx, + left: widget.offset.dy, + child: CompositedTransformFollower( + link: widget.layerLink, + showWhenUnlinked: true, + offset: widget.offset, + child: _buildToolbar(context), + ), + ); + } + + Widget _buildToolbar(BuildContext context) { + return Material( + borderRadius: BorderRadius.circular(cornerRadius), + color: const Color(0xFF333333), + child: SizedBox( + height: toolbarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _listToolbar(context), + _centerToolbarIcon('divider', width: 10), + _centerToolbarIcon('bold'), + _centerToolbarIcon('italic'), + _centerToolbarIcon('strikethrough'), + _centerToolbarIcon('underline'), + _centerToolbarIcon('divider', width: 10), + _centerToolbarIcon('quote'), + _centerToolbarIcon('number_list'), + _centerToolbarIcon('bulleted_list'), + ], + ), + ), + ); + } + + Widget _listToolbar(BuildContext context) { + return _centerToolbarIcon( + 'quote', + key: _listToolbarKey, + width: listToolbarWidth, + onTap: () => _onTapListToolbar(context), + ); + } + + Widget _centerToolbarIcon(String name, + {Key? key, double? width, VoidCallback? onTap}) { + return Tooltip( + key: key, + preferBelow: false, + message: name, + child: GestureDetector( + onTap: onTap ?? () => debugPrint('toolbar tap $name'), + child: SizedBox.fromSize( + size: width != null + ? Size(width, toolbarHeight) + : Size.square(toolbarHeight), + child: Center( + child: FlowySvg( + name: 'toolbar/$name', + ), + ), + ), + ), + ); + } + + void _onTapListToolbar(BuildContext context) { + // TODO: implement more detailed UI. + final items = defaultListToolbarEventHandlers + .map((handler) => handler.keys.first) + .toList(growable: false); + final renderBox = + _listToolbarKey.currentContext?.findRenderObject() as RenderBox; + final offset = renderBox + .localToGlobal(Offset.zero) + .translate(0, toolbarHeight - cornerRadius); + final rect = offset & Size(listToolbarWidth, listToolbarHeight); + + _listToolbarOverlay?.remove(); + _listToolbarOverlay = OverlayEntry(builder: (context) { + return Positioned.fromRect( + rect: rect, + child: Material( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(cornerRadius), + bottomRight: Radius.circular(cornerRadius), + ), + color: const Color(0xFF333333), + child: SingleChildScrollView( + child: ListView.builder( + itemExtent: toolbarHeight, + padding: const EdgeInsets.only(bottom: 10.0), + shrinkWrap: true, + itemCount: items.length, + itemBuilder: ((context, index) { + return ListTile( + contentPadding: const EdgeInsets.only( + left: 3.0, + right: 3.0, + ), + minVerticalPadding: 0.0, + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + items[index], + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + onTap: () { + debugPrint('tap on $index'); + }, + ); + }), + ), + ), + ), + ); + }); + Overlay.of(context)?.insert(_listToolbarOverlay!); + } + + void _onSelectionChange() { + _listToolbarOverlay?.remove(); + _listToolbarOverlay = null; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 71143a3ede..6d21699625 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart'; +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'; @@ -10,7 +10,6 @@ import 'package:flowy_editor/render/rich_text/flowy_rich_text.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/render/selection/floating_shortcut_widget.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; @@ -19,7 +18,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handle import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; NodeWidgetBuilders defaultBuilders = { 'editor': EditorEntryWidgetBuilder(), @@ -46,7 +45,6 @@ class FlowyEditor extends StatefulWidget { required this.editorState, this.customBuilders = const {}, this.keyEventHandlers = const [], - this.shortcuts = const [], }) : super(key: key); final EditorState editorState; @@ -57,9 +55,6 @@ class FlowyEditor extends StatefulWidget { /// Keyboard event handlers. final List keyEventHandlers; - /// Shortcuts - final FloatingShortcuts shortcuts; - @override State createState() => _FlowyEditorState(); } @@ -98,11 +93,9 @@ class _FlowyEditorState extends State { ...widget.keyEventHandlers, ], editorState: editorState, - child: FloatingShortcut( - key: editorState.service.floatingShortcutServiceKey, - size: const Size(200, 150), // TODO: support customize size. + child: FlowyToolbar( + key: editorState.service.toolbarServiceKey, editorState: editorState, - floatingShortcuts: widget.shortcuts, child: editorState.service.renderPluginService.buildPluginWidget( NodeWidgetContext( context: context, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delele_text_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 375ebee31c..f4034d4518 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -9,7 +9,7 @@ import 'package:flowy_editor/render/selection/selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/gestures.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flutter/material.dart'; @@ -452,9 +452,7 @@ class _FlowySelectionState extends State ..forEach((overlay) => overlay.remove()) ..clear(); // clear floating shortcuts - editorState.service.floatingShortcutServiceKey.currentState - ?.unwrapOrNull() - ?.hide(); + editorState.service.toolbarService.hide(); } void _updateSelection(Selection selection) { @@ -464,6 +462,9 @@ class _FlowySelectionState extends State currentSelection = selection; currentSelectedNodes.value = nodes; + Rect? topmostRect; + LayerLink? layerLink; + var index = 0; for (final node in nodes) { final selectable = node.selectable; @@ -502,19 +503,28 @@ class _FlowySelectionState extends State final rects = selectable.getRectsInSelection(newSelection); for (final rect in rects) { + // FIXME: Need to compute more precise location. + topmostRect ??= rect; + layerLink ??= node.layerLink; + _rects.add(_transformRectToGlobal(selectable, rect)); final overlay = OverlayEntry( - builder: ((context) => SelectionWidget( - color: widget.selectionColor, - layerLink: node.layerLink, - rect: rect, - )), + builder: (context) => SelectionWidget( + color: widget.selectionColor, + layerLink: node.layerLink, + rect: rect, + ), ); _selectionOverlays.add(overlay); } index += 1; } Overlay.of(context)?.insertAll(_selectionOverlays); + + if (topmostRect != null && layerLink != null) { + editorState.service.toolbarService + .showInOffset(topmostRect.topLeft, layerLink); + } } Rect _transformRectToGlobal(Selectable selectable, Rect r) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 551002499d..93c103c40f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,5 +1,5 @@ import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; @@ -22,13 +22,11 @@ class FlowyService { late FlowyRenderPlugin renderPluginService; // floating shortcut service - final floatingShortcutServiceKey = + final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); - FlowyFloatingShortcutService get floatingToolbarService { - assert(floatingShortcutServiceKey.currentState != null && - floatingShortcutServiceKey.currentState - is FlowyFloatingShortcutService); - return floatingShortcutServiceKey.currentState! - as FlowyFloatingShortcutService; + ToolbarService get toolbarService { + assert(toolbarServiceKey.currentState != null && + toolbarServiceKey.currentState is ToolbarService); + return toolbarServiceKey.currentState! as ToolbarService; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart deleted file mode 100644 index 774d906acc..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; -import 'package:flutter/material.dart'; - -mixin FlowyFloatingShortcutService { - /// Show the floating shortcut widget beside the offset. - void showInOffset(Offset offset, LayerLink layerLink); - - /// Hide the floating shortcut widget. - void hide(); -} - -class FloatingShortcut extends StatefulWidget { - const FloatingShortcut({ - Key? key, - required this.size, - required this.editorState, - required this.floatingShortcuts, - required this.child, - }) : super(key: key); - - final Size size; - final EditorState editorState; - final Widget child; - final FloatingShortcuts floatingShortcuts; - - @override - State createState() => _FloatingShortcutState(); -} - -class _FloatingShortcutState extends State - with FlowyFloatingShortcutService { - OverlayEntry? _floatintShortcutOverlay; - - @override - void showInOffset(Offset offset, LayerLink layerLink) { - _floatintShortcutOverlay?.remove(); - _floatintShortcutOverlay = OverlayEntry( - builder: (context) => FloatingShortcutWidget( - editorState: widget.editorState, - layerLink: layerLink, - rect: offset.translate(10, 0) & widget.size, - floatingShortcuts: widget.floatingShortcuts), - ); - Overlay.of(context)?.insert(_floatintShortcutOverlay!); - } - - @override - void hide() { - _floatintShortcutOverlay?.remove(); - _floatintShortcutOverlay = null; - } - - @override - Widget build(BuildContext context) { - return Container( - child: widget.child, - ); - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart new file mode 100644 index 0000000000..794e4da72b --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -0,0 +1,56 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/render/selection/toolbar_widget.dart'; +import 'package:flutter/material.dart'; + +mixin ToolbarService { + /// Show the floating shortcut widget beside the offset. + void showInOffset(Offset offset, LayerLink layerLink); + + /// Hide the floating shortcut widget. + void hide(); +} + +class FlowyToolbar extends StatefulWidget { + const FlowyToolbar({ + Key? key, + required this.editorState, + required this.child, + }) : super(key: key); + + final EditorState editorState; + final Widget child; + + @override + State createState() => _FlowyToolbarState(); +} + +class _FlowyToolbarState extends State with ToolbarService { + OverlayEntry? _floatingShortcutOverlay; + + @override + void showInOffset(Offset offset, LayerLink layerLink) { + _floatingShortcutOverlay?.remove(); + _floatingShortcutOverlay = OverlayEntry( + builder: (context) => ToolbarWidget( + editorState: widget.editorState, + layerLink: layerLink, + offset: offset.translate(0, -37.0), + handlers: const [], + ), + ); + Overlay.of(context)?.insert(_floatingShortcutOverlay!); + } + + @override + void hide() { + _floatingShortcutOverlay?.remove(); + _floatingShortcutOverlay = null; + } + + @override + Widget build(BuildContext context) { + return Container( + child: widget.child, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 403ee2dddf..db0eef5296 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -26,7 +26,7 @@ dev_dependencies: flutter: # To add assets to your package, add an assets section, like this: assets: - - assets/images/uncheck.svg + - assets/images/toolbar/ - assets/images/ - assets/document.json # - images/a_dot_burr.jpeg