diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index b6fc3467dc..fe74b22dad 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -37,7 +37,11 @@ "type": "text", "delta": [ { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + "insert": "At " + }, + { "insert": "AppFlowy", "attributes": { "code": true, "bold": true, "color": "0xFFED459C"} }, + { + "insert": ", we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." } ] }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart index fa32743b02..9be82fa31a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart @@ -33,7 +33,7 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: node.children .map( (child) => diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart index 75cde60e39..05da8d1e80 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -3,6 +3,7 @@ import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.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'; @@ -56,21 +57,22 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Row( - children: [ - FlowySvg( - size: Size.square(leftPadding), - name: 'point', - ), - Expanded( - child: FlowyRichText( + return SizedBox( + width: maxTextNodeWidth, + child: Row( + children: [ + FlowySvg( + size: Size.square(leftPadding), + name: 'point', + ), + FlowyRichText( key: _richTextKey, placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, ), - ), - ], + ], + ), ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 317d1a6bdf..89c314eb8a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -65,33 +65,34 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - child: FlowySvg( - size: Size.square(leftPadding), - name: check ? 'check' : 'uncheck', + return SizedBox( + width: maxTextNodeWidth, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: FlowySvg( + size: Size.square(leftPadding), + name: check ? 'check' : 'uncheck', + ), + onTap: () { + debugPrint('[Checkbox] onTap...'); + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + StyleKey.checkbox: !check, + }) + ..commit(); + }, ), - onTap: () { - debugPrint('[Checkbox] onTap...'); - TransactionBuilder(widget.editorState) - ..updateNode(widget.textNode, { - StyleKey.checkbox: !check, - }) - ..commit(); - }, - ), - Expanded( - child: FlowyRichText( + FlowyRichText( key: _richTextKey, placeholderText: 'To-do', textNode: widget.textNode, textSpanDecorator: _textSpanDecorator, editorState: widget.editorState, ), - ), - ], + ], + ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 32715d8556..c83ef5c31a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -11,22 +11,6 @@ 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'; -class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { - @override - Widget build(NodeWidgetContext context) { - return FlowyRichText( - key: context.node.key, - textNode: context.node, - editorState: context.editorState, - ); - } - - @override - NodeValidator get nodeValidator => ((node) { - return true; - }); -} - typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); class FlowyRichText extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart index 2511349e4d..f74064fac6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -56,25 +56,22 @@ class _HeadingTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: EdgeInsets.only( - top: topPadding, - bottom: bottomPadding, - ), - child: Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Heading', - placeholderTextSpanDecorator: _placeholderTextSpanDecorator, - textSpanDecorator: _textSpanDecorator, - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ) - ], + return SizedBox( + width: maxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only( + top: topPadding, + bottom: bottomPadding, + ), + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Heading', + placeholderTextSpanDecorator: _placeholderTextSpanDecorator, + textSpanDecorator: _textSpanDecorator, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index e9fed70c54..0a2cd5937a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -57,21 +57,22 @@ class _NumberListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Row( - children: [ - FlowySvg( - size: Size.square(leftPadding), - number: widget.textNode.attributes.number, - ), - Expanded( - child: FlowyRichText( + return SizedBox( + width: maxTextNodeWidth, + child: Row( + children: [ + FlowySvg( + size: Size.square(leftPadding), + number: widget.textNode.attributes.number, + ), + FlowyRichText( key: _richTextKey, placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, ), - ), - ], + ], + ), ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart index 00bb393652..65cfa3d066 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -56,24 +56,25 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Row( - children: [ - FlowySvg( - size: Size( - leftPadding, - _quoteHeight, + return SizedBox( + width: maxTextNodeWidth, + child: Row( + children: [ + FlowySvg( + size: Size( + leftPadding, + _quoteHeight, + ), + name: 'quote', ), - name: 'quote', - ), - Expanded( - child: FlowyRichText( + FlowyRichText( key: _richTextKey, placeholderText: 'Quote', textNode: widget.textNode, editorState: widget.editorState, ), - ), - ], + ], + ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart new file mode 100644 index 0000000000..b6d79a2358 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart @@ -0,0 +1,68 @@ +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/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.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'; + +class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return RichTextNodeWidget( + key: context.node.key, + textNode: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => ((node) { + return true; + }); +} + +class RichTextNodeWidget extends StatefulWidget { + const RichTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _RichTextNodeWidgetState(); +} + +// customize + +class _RichTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'rich_text'); + final leftPadding = 20.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset.zero; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: maxTextNodeWidth, + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index cc4f6038ac..2fb12d68ac 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -59,6 +59,8 @@ class StyleKey { ]; } +// TODO: customize +double maxTextNodeWidth = 780.0; double baseFontSize = 16.0; // TODO: customize. Map headingToFontSize = { @@ -146,7 +148,7 @@ extension DeltaAttributesExtensions on Attributes { return null; } - Color? get hightlightColor { + Color? get highlightColor { if (containsKey(StyleKey.highlightColor) && this[StyleKey.highlightColor] is String) { return Color( @@ -183,19 +185,30 @@ class RichTextStyle { return TextSpan( text: text, style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - fontSize: fontSize, - color: textColor, - backgroundColor: backgroundColor, - decoration: textDecoration, + fontWeight: _fontWeight, + fontStyle: _fontStyle, + fontSize: _fontSize, + color: _textColor, + decoration: _textDecoration, + background: _background, ), - recognizer: recognizer, + recognizer: _recognizer, ); } + Paint? get _background { + if (_backgroundColor != null) { + return Paint() + ..color = _backgroundColor! + ..strokeWidth = 24.0 + ..style = PaintingStyle.fill + ..strokeJoin = StrokeJoin.round; + } + return null; + } + // bold - FontWeight get fontWeight { + FontWeight get _fontWeight { if (attributes.bold) { return FontWeight.bold; } @@ -203,38 +216,46 @@ class RichTextStyle { } // underline or strikethrough - TextDecoration get textDecoration { + TextDecoration get _textDecoration { + var decorations = [TextDecoration.none]; if (attributes.underline || attributes.href != null) { - return TextDecoration.underline; - } else if (attributes.strikethrough) { - return TextDecoration.lineThrough; + decorations.add(TextDecoration.underline); + // TextDecoration.underline; } - return TextDecoration.none; + if (attributes.strikethrough) { + decorations.add(TextDecoration.lineThrough); + } + return TextDecoration.combine(decorations); } // font - FontStyle get fontStyle => + FontStyle get _fontStyle => attributes.italic ? FontStyle.italic : FontStyle.normal; // text color - Color get textColor { + Color get _textColor { if (attributes.href != null) { return Colors.lightBlue; } return attributes.color ?? Colors.black; } - Color get backgroundColor { - return attributes.hightlightColor ?? Colors.transparent; + Color? get _backgroundColor { + if (attributes.highlightColor != null) { + return attributes.highlightColor!; + } else if (attributes.code) { + return Colors.grey.withOpacity(0.4); + } + return null; } // font size - double get fontSize { + double get _fontSize { return baseFontSize; } // recognizer - GestureRecognizer? get recognizer { + GestureRecognizer? get _recognizer { final href = attributes.href; if (href != null) { return TapGestureRecognizer() diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 46c7d3278f..ce6d733d73 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -97,11 +97,6 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey) { Attributes attributes = { styleKey: value, }; - if (styleKey == StyleKey.underline && value) { - attributes[StyleKey.strikethrough] = null; - } else if (styleKey == StyleKey.strikethrough && value) { - attributes[StyleKey.underline] = null; - } return formatRichTextStyle(editorState, attributes); } 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 78f7bb76fa..38156dbb6b 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 @@ -4,10 +4,10 @@ import 'package:flowy_editor/editor_state.dart'; 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/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/rich_text/rich_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'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index cf71830386..d2d3b14450 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +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'; @@ -91,7 +92,8 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = ..insertNode( textNode.path.next, textNode.copyWith( - attributes: needCopyAttributes ? textNode.attributes : {}, + attributes: + needCopyAttributes ? Attributes.from(textNode.attributes) : {}, delta: textNode.delta.slice(selection.end.offset), ), ) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 220643cf6f..02073563eb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -23,6 +23,18 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { case 'b': formatBold(editorState); return KeyEventResult.handled; + case 'I': + case 'i': + formatItalic(editorState); + return KeyEventResult.handled; + case 'U': + case 'u': + formatUnderline(editorState); + return KeyEventResult.handled; + case 'S': + case 's': + formatStrikethrough(editorState); + return KeyEventResult.handled; default: break; }