diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index fecec3d3e0..f6c2fd21ff 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:example/plugin/code_block_node_widget.dart'; +import 'package:example/plugin/horizontal_rule_node_widget.dart'; import 'package:example/plugin/tex_block_node_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -121,15 +122,18 @@ class _MyHomePageState extends State { customBuilders: { 'text/code_block': CodeBlockNodeWidgetBuilder(), 'tex': TeXBlockNodeWidgetBuidler(), + 'horizontal_rule': HorizontalRuleWidgetBuilder(), }, shortcutEvents: [ enterInCodeBlock, ignoreKeysInCodeBlock, underscoreToItalic, + insertHorizontalRule, ], selectionMenuItems: [ codeBlockMenuItem, teXBlockMenuItem, + horizontalRuleMenuItem, ], ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart index 990edf9298..c40a3f0ece 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart @@ -46,7 +46,11 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) { SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( name: () => 'Code Block', - icon: const Icon(Icons.abc), + icon: const Icon( + Icons.abc, + color: Colors.black, + size: 18.0, + ), keywords: ['code block'], handler: (editorState, _, __) { final selection = diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart new file mode 100644 index 0000000000..fca3df7b64 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/horizontal_rule_node_widget.dart @@ -0,0 +1,167 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +ShortcutEvent insertHorizontalRule = ShortcutEvent( + key: 'Horizontal rule', + command: 'Minus', + handler: _insertHorzaontalRule, +); + +ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1 || selection == null) { + return KeyEventResult.ignored; + } + final textNode = textNodes.first; + if (textNode.toRawString() == '--') { + TransactionBuilder(editorState) + ..deleteText(textNode, 0, 2) + ..insertNode( + textNode.path, + Node( + type: 'horizontal_rule', + children: LinkedList(), + attributes: {}, + ), + ) + ..afterSelection = + Selection.single(path: textNode.path.next, startOffset: 0) + ..commit(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; + +SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( + name: () => 'Horizontal rule', + icon: const Icon( + Icons.horizontal_rule, + color: Colors.black, + size: 18.0, + ), + keywords: ['horizontal rule'], + handler: (editorState, _, __) { + final selection = + editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (selection == null || textNodes.isEmpty) { + return; + } + final textNode = textNodes.first; + if (textNode.toRawString().isEmpty) { + TransactionBuilder(editorState) + ..insertNode( + textNode.path, + Node( + type: 'horizontal_rule', + children: LinkedList(), + attributes: {}, + ), + ) + ..afterSelection = + Selection.single(path: textNode.path.next, startOffset: 0) + ..commit(); + } else { + TransactionBuilder(editorState) + ..insertNode( + selection.end.path.next, + TextNode( + type: 'text', + children: LinkedList(), + attributes: { + 'subtype': 'horizontal_rule', + }, + delta: Delta()..insert('---'), + ), + ) + ..afterSelection = selection + ..commit(); + } + }, +); + +class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return _HorizontalRuleWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => (node) { + return true; + }; +} + +class _HorizontalRuleWidget extends StatefulWidget { + const _HorizontalRuleWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + final Node node; + final EditorState editorState; + + @override + State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState(); +} + +class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget> + with SelectableMixin { + RenderBox get _renderBox => context.findRenderObject() as RenderBox; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Container( + height: 1, + color: Colors.grey, + ), + ); + } + + @override + Position start() => Position(path: widget.node.path, offset: 0); + + @override + Position end() => Position(path: widget.node.path, offset: 1); + + @override + Position getPositionInOffset(Offset start) => end(); + + @override + bool get shouldCursorBlink => false; + + @override + CursorStyle get cursorStyle => CursorStyle.borderLine; + + @override + Rect? getCursorRectInPosition(Position position) { + final size = _renderBox.size; + return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height); + } + + @override + List getRectsInSelection(Selection selection) => + [Offset.zero & _renderBox.size]; + + @override + Selection getSelectionInRange(Offset start, Offset end) => Selection.single( + path: widget.node.path, + startOffset: 0, + endOffset: 1, + ); + + @override + Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart index e76c3e160b..ac40a31508 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/tex_block_node_widget.dart @@ -6,7 +6,11 @@ import 'package:flutter_math_fork/flutter_math.dart'; SelectionMenuItem teXBlockMenuItem = SelectionMenuItem( name: () => 'Tex', - icon: const Icon(Icons.text_fields_rounded), + icon: const Icon( + Icons.text_fields_rounded, + color: Colors.black, + size: 18.0, + ), keywords: ['tex, latex, katex'], handler: (editorState, _, __) { final selection = diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/cursor_widget.dart index 19da4b55f4..a7b68d410d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/cursor_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/cursor_widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; class CursorWidget extends StatefulWidget { @@ -9,9 +10,13 @@ class CursorWidget extends StatefulWidget { required this.rect, required this.color, this.blinkingInterval = 0.5, + this.shouldBlink = true, + this.cursorStyle = CursorStyle.verticalLine, }) : super(key: key); final double blinkingInterval; // milliseconds + final bool shouldBlink; + final CursorStyle cursorStyle; final Color color; final Rect rect; final LayerLink layerLink; @@ -67,11 +72,28 @@ class CursorWidgetState extends State { // Ignore the gestures in cursor // to solve the problem that cursor area cannot be selected. child: IgnorePointer( - child: Container( - color: showCursor ? widget.color : Colors.transparent, - ), + child: _buildCursor(context), ), ), ); } + + Widget _buildCursor(BuildContext context) { + var color = widget.color; + if (widget.shouldBlink && !showCursor) { + color = Colors.transparent; + } + switch (widget.cursorStyle) { + case CursorStyle.verticalLine: + return Container( + color: color, + ); + case CursorStyle.borderLine: + return Container( + decoration: BoxDecoration( + border: Border.all(color: color, width: 2), + ), + ); + } + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart index 372dbd7067..6f4f92c2e9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart @@ -2,6 +2,11 @@ import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/selection.dart'; import 'package:flutter/material.dart'; +enum CursorStyle { + verticalLine, + borderLine, +} + /// [SelectableMixin] is used for the editor to calculate the position /// and size of the selection. /// @@ -53,4 +58,8 @@ mixin SelectableMixin on State { Selection? getWorldBoundaryInOffset(Offset start) { return null; } + + bool get shouldCursorBlink => true; + + CursorStyle get cursorStyle => CursorStyle.verticalLine; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 3f7a54810f..229d011743 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -3,7 +3,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_l import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/extensions/path_extensions.dart'; // Handle delete text. ShortcutEventHandler deleteTextHandler = (editorState, event) { @@ -84,6 +83,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } } else { if (textNodes.isEmpty) { + if (nonTextNodes.isNotEmpty) { + transactionBuilder.afterSelection = + Selection.collapsed(selection.start); + } + transactionBuilder.commit(); return KeyEventResult.handled; } final startPosition = selection.start; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index d9b5422aa1..4eacc5674c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -457,6 +457,8 @@ class _AppFlowySelectionState extends State rect: cursorRect, color: widget.cursorColor, layerLink: node.layerLink, + shouldBlink: selectable.shouldCursorBlink, + cursorStyle: selectable.cursorStyle, ), );