diff --git a/packages/fleather/lib/src/rendering/editable_text_line.dart b/packages/fleather/lib/src/rendering/editable_text_line.dart index e3073a88..36b8600a 100644 --- a/packages/fleather/lib/src/rendering/editable_text_line.dart +++ b/packages/fleather/lib/src/rendering/editable_text_line.dart @@ -14,7 +14,7 @@ import 'editable_box.dart'; const double _kCursorHeightOffset = 2.0; // pixels -enum TextLineSlot { leading, body } +enum TextLineSlot { leading, body, trailing } class RenderEditableTextLine extends RenderEditableBox { /// Creates new editable paragraph render box. @@ -167,6 +167,9 @@ class RenderEditableTextLine extends RenderEditableBox { if (_body != null) { yield _body!; } + if (_trailing != null) { + yield _trailing!; + } } RenderBox? get leading => _leading; @@ -176,6 +179,13 @@ class RenderEditableTextLine extends RenderEditableBox { _leading = _updateChild(_leading, value, TextLineSlot.leading); } + RenderBox? get trailing => _trailing; + RenderBox? _trailing; + + set trailing(RenderBox? value) { + _trailing = _updateChild(_trailing, value, TextLineSlot.trailing); + } + RenderContentProxyBox? get body => _body; RenderContentProxyBox? _body; @@ -573,6 +583,7 @@ class RenderEditableTextLine extends RenderEditableBox { add(leading, 'leading'); add(body, 'body'); + add(trailing, 'trailing'); return value; } @@ -586,8 +597,9 @@ class RenderEditableTextLine extends RenderEditableBox { final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final effectiveHeight = math.max(0.0, height - verticalPadding); final leadingWidth = leading?.getMinIntrinsicWidth(effectiveHeight) ?? 0; + final trailingWidth = trailing?.getMinIntrinsicWidth(effectiveHeight) ?? 0; final bodyWidth = body?.getMinIntrinsicWidth(effectiveHeight) ?? 0; - return horizontalPadding + leadingWidth + bodyWidth; + return horizontalPadding + leadingWidth + bodyWidth + trailingWidth; } @override @@ -597,8 +609,9 @@ class RenderEditableTextLine extends RenderEditableBox { final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; final effectiveHeight = math.max(0.0, height - verticalPadding); final leadingWidth = leading?.getMaxIntrinsicWidth(effectiveHeight) ?? 0; + final trailingWidth = trailing?.getMaxIntrinsicWidth(effectiveHeight) ?? 0; final bodyWidth = body?.getMaxIntrinsicWidth(effectiveHeight) ?? 0; - return horizontalPadding + leadingWidth + bodyWidth; + return horizontalPadding + leadingWidth + bodyWidth + trailingWidth; } @override @@ -642,7 +655,7 @@ class RenderEditableTextLine extends RenderEditableBox { _resolvePadding(); assert(_resolvedPadding != null); - if (body == null && leading == null) { + if (body == null && leading == null && trailing == null) { size = constraints.constrain(Size( _resolvedPadding!.left + _resolvedPadding!.right, _resolvedPadding!.top + _resolvedPadding!.bottom, @@ -672,6 +685,18 @@ class RenderEditableTextLine extends RenderEditableBox { parentData.offset = Offset(dxOffset, _resolvedPadding!.top); } + if (trailing != null) { + final trailingConstraints = innerConstraints.copyWith( + minWidth: indentWidth, + maxWidth: indentWidth, + maxHeight: body!.size.height); + trailing!.layout(trailingConstraints, parentUsesSize: true); + final parentData = trailing!.parentData as BoxParentData; + final dxOffset = + textDirection == TextDirection.rtl ? 0.0 : body!.size.width; + parentData.offset = Offset(dxOffset, _resolvedPadding!.top); + } + size = constraints.constrain(Size( _resolvedPadding!.left + body!.size.width + _resolvedPadding!.right, _resolvedPadding!.top + body!.size.height + _resolvedPadding!.bottom, @@ -697,6 +722,12 @@ class RenderEditableTextLine extends RenderEditableBox { context.paintChild(leading!, effectiveOffset); } + if (trailing != null) { + final parentData = trailing!.parentData as BoxParentData; + final effectiveOffset = offset + parentData.offset; + context.paintChild(trailing!, effectiveOffset); + } + if (body != null) { final parentData = body!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; @@ -806,6 +837,16 @@ class RenderEditableTextLine extends RenderEditableBox { ); if (isHit) return true; } + if (trailing != null) { + final childParentData = trailing!.parentData as BoxParentData; + final isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (result, transformed) => + trailing!.hitTest(result, position: transformed), + ); + if (isHit) return true; + } if (body == null) return false; final parentData = body!.parentData as BoxParentData; final offset = position - parentData.offset; diff --git a/packages/fleather/lib/src/widgets/code_color.dart b/packages/fleather/lib/src/widgets/code_color.dart new file mode 100644 index 00000000..3419fee5 --- /dev/null +++ b/packages/fleather/lib/src/widgets/code_color.dart @@ -0,0 +1,137 @@ +import 'package:flutter/widgets.dart'; +import 'package:highlight/highlight.dart' show highlight, Node; + +/// use highlight to color the code +/// the default theme is github theme +class CodeColor { + static const _rootKey = 'root'; + static const _defaultFontColor = Color(0xff000000); + + TextSpan textSpan(String source, bool isDark) { + var theme = isDark ? vs2015Theme : githubTheme; + var textStyle = TextStyle( + color: theme[_rootKey]?.color ?? _defaultFontColor, + ); + + return TextSpan( + style: textStyle, + children: + _convert(highlight.parse(source, language: 'java').nodes!, theme), + ); + } + + List _convert(List nodes, Map theme) { + List spans = []; + var currentSpans = spans; + List> stack = []; + + void traverse(Node node) { + if (node.value != null) { + currentSpans.add(node.className == null + ? TextSpan(text: node.value) + : TextSpan(text: node.value, style: theme[node.className!])); + } else if (node.children != null) { + List tmp = []; + currentSpans + .add(TextSpan(children: tmp, style: theme[node.className!])); + stack.add(currentSpans); + currentSpans = tmp; + + for (var n in node.children!) { + traverse(n); + if (n == node.children!.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + } + } + } + + for (var node in nodes) { + traverse(node); + } + + return spans; + } +} + +const vs2015Theme = { + 'root': + TextStyle(backgroundColor: Color(0xff1E1E1E), color: Color(0xffDCDCDC)), + 'keyword': TextStyle(color: Color(0xff569CD6)), + 'literal': TextStyle(color: Color(0xff569CD6)), + 'symbol': TextStyle(color: Color(0xff569CD6)), + 'name': TextStyle(color: Color(0xff569CD6)), + 'link': TextStyle(color: Color(0xff569CD6)), + 'built_in': TextStyle(color: Color(0xff4EC9B0)), + 'type': TextStyle(color: Color(0xff4EC9B0)), + 'number': TextStyle(color: Color(0xffB8D7A3)), + 'class': TextStyle(color: Color(0xffB8D7A3)), + 'string': TextStyle(color: Color(0xffD69D85)), + 'meta-string': TextStyle(color: Color(0xffD69D85)), + 'regexp': TextStyle(color: Color(0xff9A5334)), + 'template-tag': TextStyle(color: Color(0xff9A5334)), + 'subst': TextStyle(color: Color(0xffDCDCDC)), + 'function': TextStyle(color: Color(0xffDCDCDC)), + 'title': TextStyle(color: Color(0xffDCDCDC)), + 'params': TextStyle(color: Color(0xffDCDCDC)), + 'formula': TextStyle(color: Color(0xffDCDCDC)), + 'comment': TextStyle(color: Color(0xff57A64A), fontStyle: FontStyle.italic), + 'quote': TextStyle(color: Color(0xff57A64A), fontStyle: FontStyle.italic), + 'doctag': TextStyle(color: Color(0xff608B4E)), + 'meta': TextStyle(color: Color(0xff9B9B9B)), + 'meta-keyword': TextStyle(color: Color(0xff9B9B9B)), + 'tag': TextStyle(color: Color(0xff9B9B9B)), + 'variable': TextStyle(color: Color(0xffBD63C5)), + 'template-variable': TextStyle(color: Color(0xffBD63C5)), + 'attr': TextStyle(color: Color(0xff9CDCFE)), + 'attribute': TextStyle(color: Color(0xff9CDCFE)), + 'builtin-name': TextStyle(color: Color(0xff9CDCFE)), + 'section': TextStyle(color: Color(0xffffd700)), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), + 'strong': TextStyle(fontWeight: FontWeight.bold), + 'bullet': TextStyle(color: Color(0xffD7BA7D)), + 'selector-tag': TextStyle(color: Color(0xffD7BA7D)), + 'selector-id': TextStyle(color: Color(0xffD7BA7D)), + 'selector-class': TextStyle(color: Color(0xffD7BA7D)), + 'selector-attr': TextStyle(color: Color(0xffD7BA7D)), + 'selector-pseudo': TextStyle(color: Color(0xffD7BA7D)), + 'addition': TextStyle(backgroundColor: Color(0xff144212)), + 'deletion': TextStyle(backgroundColor: Color(0xff660000)), +}; + +const githubTheme = { + 'root': + TextStyle(color: Color(0xff333333), backgroundColor: Color(0xfff8f8f8)), + 'comment': TextStyle(color: Color(0xff999988), fontStyle: FontStyle.italic), + 'quote': TextStyle(color: Color(0xff999988), fontStyle: FontStyle.italic), + 'keyword': TextStyle(color: Color(0xff333333), fontWeight: FontWeight.bold), + 'selector-tag': + TextStyle(color: Color(0xff333333), fontWeight: FontWeight.bold), + 'subst': TextStyle(color: Color(0xff333333), fontWeight: FontWeight.normal), + 'number': TextStyle(color: Color(0xff008080)), + 'literal': TextStyle(color: Color(0xff008080)), + 'variable': TextStyle(color: Color(0xff008080)), + 'template-variable': TextStyle(color: Color(0xff008080)), + 'string': TextStyle(color: Color(0xffdd1144)), + 'doctag': TextStyle(color: Color(0xffdd1144)), + 'title': TextStyle(color: Color(0xff990000), fontWeight: FontWeight.bold), + 'section': TextStyle(color: Color(0xff990000), fontWeight: FontWeight.bold), + 'selector-id': + TextStyle(color: Color(0xff990000), fontWeight: FontWeight.bold), + 'type': TextStyle(color: Color(0xff445588), fontWeight: FontWeight.bold), + 'tag': TextStyle(color: Color(0xff000080), fontWeight: FontWeight.normal), + 'name': TextStyle(color: Color(0xff000080), fontWeight: FontWeight.normal), + 'attribute': + TextStyle(color: Color(0xff000080), fontWeight: FontWeight.normal), + 'regexp': TextStyle(color: Color(0xff009926)), + 'link': TextStyle(color: Color(0xff009926)), + 'symbol': TextStyle(color: Color(0xff990073)), + 'bullet': TextStyle(color: Color(0xff990073)), + 'built_in': TextStyle(color: Color(0xff0086b3)), + 'builtin-name': TextStyle(color: Color(0xff0086b3)), + 'meta': TextStyle(color: Color(0xff999999), fontWeight: FontWeight.bold), + 'deletion': TextStyle(backgroundColor: Color(0xffffdddd)), + 'addition': TextStyle(backgroundColor: Color(0xffddffdd)), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), + 'strong': TextStyle(fontWeight: FontWeight.bold), +}; diff --git a/packages/fleather/lib/src/widgets/editable_text_block.dart b/packages/fleather/lib/src/widgets/editable_text_block.dart index a4bb4f04..6a54ba74 100644 --- a/packages/fleather/lib/src/widgets/editable_text_block.dart +++ b/packages/fleather/lib/src/widgets/editable_text_block.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:parchment/parchment.dart'; +import 'package:flutter/services.dart'; import '../../util.dart'; import '../rendering/editable_text_block.dart'; @@ -74,6 +75,7 @@ class EditableTextBlock extends StatelessWidget { node: line, spacing: _getSpacingForLine(line, index, count, theme), leading: leadingWidgets?[index], + trailing: index == 0 ? _buildCopyButton(context, lineNodes) : null, indentWidth: _getIndentWidth(line), devicePixelRatio: MediaQuery.of(context).devicePixelRatio, body: TextLine( @@ -96,6 +98,31 @@ class EditableTextBlock extends StatelessWidget { return children.toList(growable: false); } + Widget? _buildCopyButton(BuildContext context, List lineNodes) { + final block = node.style.get(ParchmentAttribute.block); + if (block != ParchmentAttribute.block.code) { + return null; + } + return InkWell( + onTap: () { + List lines = []; + lines = lineNodes.map((e) => e.toPlainText().trimRight()).toList(); + Clipboard.setData(ClipboardData(text: lines.join('\n'))); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(const SnackBar( + content: Text('Copy code successfully'), + duration: Duration(seconds: 1), + )); + }, + child: const Icon( + Icons.copy, + size: 16.0, + color: Colors.grey, + ), + ); + } + List? _buildLeading( FleatherThemeData theme, List children) { final block = node.style.get(ParchmentAttribute.block); @@ -137,7 +164,7 @@ class EditableTextBlock extends StatelessWidget { number: i + 1, style: theme.code.style .copyWith(color: theme.code.style.color?.withOpacity(0.4)), - width: 32.0, + width: 42.0, padding: 16.0, withDot: false, )) @@ -164,7 +191,7 @@ class EditableTextBlock extends StatelessWidget { leadingWidgets.add(_NumberPoint( number: currentIndex + 1, style: theme.lists.style, - width: 32.0, + width: 42.0, padding: 8.0, )); levelsIndexes[currentLevel] = currentIndex; @@ -183,7 +210,7 @@ class EditableTextBlock extends StatelessWidget { if (block == ParchmentAttribute.block.quote) { return extraIndent + 16.0; } else { - return extraIndent + 32.0; + return extraIndent + 42.0; } } diff --git a/packages/fleather/lib/src/widgets/editable_text_line.dart b/packages/fleather/lib/src/widgets/editable_text_line.dart index bc31ce4c..b03d3d75 100644 --- a/packages/fleather/lib/src/widgets/editable_text_line.dart +++ b/packages/fleather/lib/src/widgets/editable_text_line.dart @@ -17,6 +17,9 @@ class EditableTextLine extends RenderObjectWidget { /// A widget to display before the body. final Widget? leading; + /// A widget to display after the body. + final Widget? trailing; + /// The primary rich text content of this widget. Usually [TextLine] widget. final Widget body; @@ -45,6 +48,7 @@ class EditableTextLine extends RenderObjectWidget { required this.hasFocus, required this.devicePixelRatio, this.leading, + this.trailing, // 添加 trailing 参数 this.indentWidth = 0.0, this.spacing = const VerticalSpacing(), }); @@ -134,6 +138,7 @@ class _RenderEditableTextLineElement extends RenderObjectElement { super.mount(parent, newSlot); _mountChild(widget.leading, TextLineSlot.leading); _mountChild(widget.body, TextLineSlot.body); + _mountChild(widget.trailing, TextLineSlot.trailing); // 添加 trailing } void _updateChild(Widget? widget, TextLineSlot slot) { @@ -153,6 +158,7 @@ class _RenderEditableTextLineElement extends RenderObjectElement { assert(widget == newWidget); _updateChild(widget.leading, TextLineSlot.leading); _updateChild(widget.body, TextLineSlot.body); + _updateChild(widget.trailing, TextLineSlot.trailing); // 添加 trailing } void _updateRenderObject(RenderObject? child, TextLineSlot? slot) { @@ -160,6 +166,9 @@ class _RenderEditableTextLineElement extends RenderObjectElement { case TextLineSlot.leading: renderObject.leading = child as RenderBox?; break; + case TextLineSlot.trailing: + renderObject.trailing = child as RenderBox?; + break; case TextLineSlot.body: renderObject.body = child as RenderContentProxyBox?; break; diff --git a/packages/fleather/lib/src/widgets/editor_toolbar.dart b/packages/fleather/lib/src/widgets/editor_toolbar.dart index e9852a5b..18bed9e9 100644 --- a/packages/fleather/lib/src/widgets/editor_toolbar.dart +++ b/packages/fleather/lib/src/widgets/editor_toolbar.dart @@ -851,6 +851,7 @@ class FleatherToolbar extends StatefulWidget implements PreferredSizeWidget { bool hideHorizontalRule = false, bool hideDirection = false, bool hideUndoRedo = false, + VoidCallback? onImagePressed, List leading = const [], List trailing = const [], bool hideAlignment = false, @@ -1122,6 +1123,14 @@ class FleatherToolbar extends StatefulWidget implements PreferredSizeWidget { Visibility( visible: !hideLink, child: LinkStyleButton(controller: controller)), + Visibility( + visible: onImagePressed != null, + child: FLIconButton( + size: 32, + onPressed: onImagePressed, + icon: const Icon(Icons.image_outlined, size: 18), + ), + ), Visibility( visible: !hideHorizontalRule, child: InsertEmbedButton( diff --git a/packages/fleather/lib/src/widgets/text_line.dart b/packages/fleather/lib/src/widgets/text_line.dart index 566120b6..e83be5bb 100644 --- a/packages/fleather/lib/src/widgets/text_line.dart +++ b/packages/fleather/lib/src/widgets/text_line.dart @@ -13,6 +13,7 @@ import 'keyboard_listener.dart'; import 'link.dart'; import 'rich_text_proxy.dart'; import 'theme.dart'; +import 'code_color.dart'; /// Line of text in Fleather editor. /// @@ -172,8 +173,13 @@ class _TextLineState extends State { final text = segment as TextNode; final attrs = text.style; final isLink = attrs.contains(ParchmentAttribute.link); + bool isCodeBlock = widget.node.style.get(ParchmentAttribute.block) == + ParchmentAttribute.block.code; + final isDark = Theme.of(context).brightness == Brightness.dark; + return TextSpan( - text: text.value, + text: isCodeBlock ? null : text.value, + children: isCodeBlock ? [CodeColor().textSpan(text.value, isDark)] : [], style: _getInlineTextStyle(attrs, widget.node.style, theme), recognizer: isLink && canLaunchLinks ? _getRecognizer(segment) : null, mouseCursor: isLink && canLaunchLinks ? SystemMouseCursors.click : null, diff --git a/packages/fleather/pubspec.yaml b/packages/fleather/pubspec.yaml index 3f198a32..bf8269c7 100644 --- a/packages/fleather/pubspec.yaml +++ b/packages/fleather/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: parchment_delta: ^1.0.0 parchment: ^1.19.0 intl: ^0.19.0 + highlight: ^0.7.0 dependency_overrides: parchment: