Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code block copy button (#433) #434

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions packages/fleather/lib/src/rendering/editable_text_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -167,6 +167,9 @@ class RenderEditableTextLine extends RenderEditableBox {
if (_body != null) {
yield _body!;
}
if (_trailing != null) {
yield _trailing!;
}
}

RenderBox? get leading => _leading;
Expand All @@ -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;

Expand Down Expand Up @@ -573,6 +583,7 @@ class RenderEditableTextLine extends RenderEditableBox {

add(leading, 'leading');
add(body, 'body');
add(trailing, 'trailing');
return value;
}

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
137 changes: 137 additions & 0 deletions packages/fleather/lib/src/widgets/code_color.dart
Original file line number Diff line number Diff line change
@@ -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<TextSpan> _convert(List<Node> nodes, Map<String, TextStyle> theme) {
List<TextSpan> spans = [];
var currentSpans = spans;
List<List<TextSpan>> 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<TextSpan> 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),
};
33 changes: 30 additions & 3 deletions packages/fleather/lib/src/widgets/editable_text_block.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
Expand All @@ -96,6 +98,31 @@ class EditableTextBlock extends StatelessWidget {
return children.toList(growable: false);
}

Widget? _buildCopyButton(BuildContext context, List<LineNode> lineNodes) {
final block = node.style.get(ParchmentAttribute.block);
if (block != ParchmentAttribute.block.code) {
return null;
}
return InkWell(
onTap: () {
List<String> 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<Widget>? _buildLeading(
FleatherThemeData theme, List<LineNode> children) {
final block = node.style.get(ParchmentAttribute.block);
Expand Down Expand Up @@ -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,
))
Expand All @@ -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;
Expand All @@ -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;
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/fleather/lib/src/widgets/editable_text_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(),
});
Expand Down Expand Up @@ -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) {
Expand All @@ -153,13 +158,17 @@ 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) {
switch (slot) {
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;
Expand Down
Loading
Loading