浏览代码

feat: increase line spacing

Lucas.Xu 2 年之前
父节点
当前提交
e9d8dc9657

+ 9 - 0
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,

+ 29 - 22
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<BulletedListTextNodeWidget>
     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<StatefulWidget> 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,
+              ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }

+ 40 - 36
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<CheckboxNodeWidget>
     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<StatefulWidget> 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<CheckboxNodeWidget>
 
   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) {

+ 11 - 1
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);

+ 12 - 10
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<HeadingTextNodeWidget>
     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<StatefulWidget> get forward =>
@@ -51,18 +53,18 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
 
   @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',

+ 30 - 25
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<NumberListTextNodeWidget>
     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<StatefulWidget> 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,
-          ),
-          Expanded(
-            child: FlowyRichText(
-              key: _richTextKey,
-              placeholderText: 'List',
-              textNode: widget.textNode,
-              editorState: widget.editorState,
-            ),
+    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,
+                ),
+              ),
+            ],
           ),
-        ],
-      ),
-    );
+        ));
   }
 }

+ 30 - 28
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<QuotedTextNodeWidget>
     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<StatefulWidget> 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',
-          ),
-          Expanded(
-            child: FlowyRichText(
-              key: _richTextKey,
-              placeholderText: 'Quote',
-              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(_iconSize, _quoteHeight),
+                padding:
+                    EdgeInsets.only(top: topPadding, right: _iconRightPadding),
+                name: 'quote',
+              ),
+              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;
   }
 }

+ 11 - 11
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<RichTextNodeWidget>
     with Selectable, DefaultSelectable {
+  @override
+  GlobalKey? get iconKey => null;
+
   final _richTextKey = GlobalKey(debugLabel: 'rich_text');
-  final leftPadding = 20.0;
 
   @override
   Selectable<StatefulWidget> 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,
+        ),
       ),
     );
   }

+ 26 - 2
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<String, double> 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() => _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() {
+  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,
     );