From e9d8dc9657b73d628550dbef1ce3c6a05bbde8df Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 17:50:32 +0800 Subject: [PATCH] feat: increase line spacing --- .../flowy_editor/lib/infra/flowy_svg.dart | 9 +++ .../render/rich_text/bulleted_list_text.dart | 51 +++++++------ .../lib/render/rich_text/checkbox_text.dart | 76 ++++++++++--------- .../render/rich_text/default_selectable.dart | 12 ++- .../lib/render/rich_text/heading_text.dart | 22 +++--- .../render/rich_text/number_list_text.dart | 55 ++++++++------ .../lib/render/rich_text/quoted_text.dart | 58 +++++++------- .../lib/render/rich_text/rich_text.dart | 22 +++--- .../lib/render/rich_text/rich_text_style.dart | 28 ++++++- 9 files changed, 198 insertions(+), 135 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart index d38fe2d16d..d40a198b1a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart @@ -8,15 +8,24 @@ class FlowySvg extends StatelessWidget { this.size = const Size(20, 20), this.color, this.number, + this.padding, }) : super(key: key); final String? name; final Size size; final Color? color; final int? number; + final EdgeInsets? padding; @override Widget build(BuildContext context) { + return Padding( + padding: padding ?? const EdgeInsets.all(0), + child: _buildSvg(), + ); + } + + Widget _buildSvg() { if (name != null) { return SizedBox.fromSize( size: size, 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 b962f63f3d..2607be26ed 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 @@ -43,38 +43,45 @@ class BulletedListTextNodeWidget extends StatefulWidget { class _BulletedListTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + final iconKey = GlobalKey(); + final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text'); - final leftPadding = 20.0; + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; + return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - size: Size.square(leftPadding), - name: 'point', - ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + size: Size.square(_iconSize), + padding: + EdgeInsets.only(top: topPadding, right: _iconRightPadding), + name: 'point', ), - ), - ], + Expanded( + child: 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 e5b02eb32d..890f80fd54 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 @@ -41,19 +41,17 @@ class CheckboxNodeWidget extends StatefulWidget { class _CheckboxNodeWidgetState extends State with Selectable, DefaultSelectable { - final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); + @override + final iconKey = GlobalKey(); - final leftPadding = 20.0; + final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { if (widget.textNode.children.isEmpty) { @@ -65,37 +63,43 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; 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(); - }, + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: FlowySvg( + key: iconKey, + size: Size.square(_iconSize), + padding: EdgeInsets.only( + top: topPadding, right: _iconRightPadding), + name: check ? 'check' : 'uncheck', + ), + onTap: () { + debugPrint('[Checkbox] onTap...'); + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + StyleKey.checkbox: !check, + }) + ..commit(); + }, + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'To-do', + textNode: widget.textNode, + textSpanDecorator: _textSpanDecorator, + editorState: widget.editorState, + ), + ), + ], ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'To-do', - textNode: widget.textNode, - textSpanDecorator: _textSpanDecorator, - editorState: widget.editorState, - ), - ), - ], - ), - ); + )); } Widget _buildWithChildren(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart index 21cc5108f3..4fbe9c1521 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart @@ -6,7 +6,17 @@ import 'package:flutter/material.dart'; mixin DefaultSelectable { Selectable get forward; - Offset get baseOffset; + GlobalKey? get iconKey; + + Offset get baseOffset { + if (iconKey != null) { + final renderBox = iconKey!.currentContext?.findRenderObject(); + if (renderBox is RenderBox) { + return Offset(renderBox.size.width, 0); + } + } + return Offset.zero; + } Position getPositionInOffset(Offset start) => forward.getPositionInOffset(start); 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 f74064fac6..c010ad4833 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 @@ -41,9 +41,11 @@ class HeadingTextNodeWidget extends StatefulWidget { class _HeadingTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + GlobalKey? get iconKey => null; + final _richTextKey = GlobalKey(debugLabel: 'heading_text'); - final topPadding = 5.0; - final bottomPadding = 2.0; + final _topPadding = 5.0; @override Selectable get forward => @@ -51,18 +53,18 @@ class _HeadingTextNodeWidgetState extends State @override Offset get baseOffset { - return Offset(0, topPadding); + return Offset(0, _topPadding); } @override Widget build(BuildContext context) { - return SizedBox( - width: maxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only( - top: topPadding, - bottom: bottomPadding, - ), + return Padding( + padding: EdgeInsets.only( + top: _topPadding, + bottom: defaultLinePadding, + ), + child: SizedBox( + width: defaultMaxTextNodeWidth, child: FlowyRichText( key: _richTextKey, placeholderText: 'Heading', 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 65b41e8e9b..4ffd587470 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 @@ -43,39 +43,44 @@ class NumberListTextNodeWidget extends StatefulWidget { class _NumberListTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + final iconKey = GlobalKey(); + final _richTextKey = GlobalKey(debugLabel: 'number_list_text'); - final leftPadding = 20.0; + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { - return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - size: Size.square(leftPadding), - number: widget.textNode.attributes.number, + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: SizedBox( + width: defaultMaxTextNodeWidth, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + size: Size.square(_iconSize), + padding: + EdgeInsets.only(top: topPadding, right: _iconRightPadding), + number: widget.textNode.attributes.number, + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), + ], ), - Expanded( - child: 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 0bb259de14..09004f7f9d 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 @@ -42,48 +42,50 @@ class QuotedTextNodeWidget extends StatefulWidget { class _QuotedTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + final iconKey = GlobalKey(); + final _richTextKey = GlobalKey(debugLabel: 'quoted_text'); - final leftPadding = 20.0; + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - size: Size( - leftPadding, - _quoteHeight, - ), - name: 'quote', + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + size: Size(_iconSize, _quoteHeight), + padding: + EdgeInsets.only(top: topPadding, right: _iconRightPadding), + name: 'quote', + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), + ], ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), - ); + )); } double get _quoteHeight { final lines = widget.textNode.toRawString().characters.where((c) => c == '\n').length; - return (lines + 1) * leftPadding; + return (lines + 1) * _iconSize; } } 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 index b6d79a2358..bfb4c217a7 100644 --- 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 @@ -42,26 +42,26 @@ class RichTextNodeWidget extends StatefulWidget { class _RichTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + GlobalKey? get iconKey => null; + 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, + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + 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 2fb12d68ac..e39aed9aa2 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 @@ -1,4 +1,5 @@ import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/document/node.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -60,7 +61,8 @@ class StyleKey { } // TODO: customize -double maxTextNodeWidth = 780.0; +double defaultMaxTextNodeWidth = 780.0; +double defaultLinePadding = 8.0; double baseFontSize = 16.0; // TODO: customize. Map headingToFontSize = { @@ -176,12 +178,33 @@ class RichTextStyle { RichTextStyle({ required this.attributes, required this.text, + this.height = 1.5, }); + RichTextStyle.fromTextNode(TextNode textNode) + : this(attributes: textNode.attributes, text: textNode.toRawString()); + final Attributes attributes; final String text; + final double height; - TextSpan toTextSpan() { + TextSpan toTextSpan() => _toTextSpan(height); + + double get topPadding { + if (height == 1.0) { + return 0; + } + // TODO: Need to be optimized. + final painter = + TextPainter(text: _toTextSpan(height), textDirection: TextDirection.ltr) + ..layout(); + final basePainter = + TextPainter(text: _toTextSpan(null), textDirection: TextDirection.ltr) + ..layout(); + return painter.height - basePainter.height; + } + + TextSpan _toTextSpan(double? height) { return TextSpan( text: text, style: TextStyle( @@ -191,6 +214,7 @@ class RichTextStyle { color: _textColor, decoration: _textDecoration, background: _background, + height: height, ), recognizer: _recognizer, );