parent
dc7b4e794f
commit
37fc173dde
9 changed files with 1178 additions and 14 deletions
@ -0,0 +1,38 @@ |
|||||||
|
import 'package:flutter/rendering.dart'; |
||||||
|
import 'package:flutter_quill/models/documents/nodes/container.dart'; |
||||||
|
|
||||||
|
abstract class RenderContentProxyBox implements RenderBox { |
||||||
|
double getPreferredLineHeight(); |
||||||
|
|
||||||
|
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype); |
||||||
|
|
||||||
|
TextPosition getPositionForOffset(Offset offset); |
||||||
|
|
||||||
|
double getFullHeightForCaret(TextPosition position); |
||||||
|
|
||||||
|
TextRange getWordBoundary(TextPosition position); |
||||||
|
|
||||||
|
List<TextBox> getBoxesForSelection(TextSelection textSelection); |
||||||
|
} |
||||||
|
|
||||||
|
abstract class RenderEditableBox extends RenderBox { |
||||||
|
Container getContainer(); |
||||||
|
|
||||||
|
double preferredLineHeight(TextPosition position); |
||||||
|
|
||||||
|
Offset getOffsetForCaret(TextPosition position); |
||||||
|
|
||||||
|
TextPosition getPositionForOffset(Offset offset); |
||||||
|
|
||||||
|
TextPosition getPositionAbove(TextPosition position); |
||||||
|
|
||||||
|
TextPosition getPositionBelow(TextPosition position); |
||||||
|
|
||||||
|
TextRange getWordBoundary(TextPosition position); |
||||||
|
|
||||||
|
TextRange getLineBoundary(TextPosition position); |
||||||
|
|
||||||
|
TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection); |
||||||
|
|
||||||
|
TextSelectionPoint getExtentEndpointForSelection(TextSelection textSelection); |
||||||
|
} |
@ -0,0 +1,134 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter/widgets.dart'; |
||||||
|
import 'package:tuple/tuple.dart'; |
||||||
|
|
||||||
|
class DefaultTextBlockStyle { |
||||||
|
final TextStyle style; |
||||||
|
|
||||||
|
final Tuple2<double, double> verticalSpacing; |
||||||
|
|
||||||
|
final Tuple2<double, double> lineSpacing; |
||||||
|
|
||||||
|
final BoxDecoration decoration; |
||||||
|
|
||||||
|
DefaultTextBlockStyle( |
||||||
|
this.style, this.verticalSpacing, this.lineSpacing, this.decoration); |
||||||
|
} |
||||||
|
|
||||||
|
class DefaultStyles { |
||||||
|
final DefaultTextBlockStyle h1; |
||||||
|
final DefaultTextBlockStyle h2; |
||||||
|
final DefaultTextBlockStyle h3; |
||||||
|
final DefaultTextBlockStyle paragraph; |
||||||
|
final TextStyle bold; |
||||||
|
final TextStyle italic; |
||||||
|
final TextStyle underline; |
||||||
|
final TextStyle strikeThrough; |
||||||
|
final TextStyle link; |
||||||
|
final DefaultTextBlockStyle lists; |
||||||
|
final DefaultTextBlockStyle quote; |
||||||
|
final DefaultTextBlockStyle code; |
||||||
|
|
||||||
|
DefaultStyles( |
||||||
|
this.h1, |
||||||
|
this.h2, |
||||||
|
this.h3, |
||||||
|
this.paragraph, |
||||||
|
this.bold, |
||||||
|
this.italic, |
||||||
|
this.underline, |
||||||
|
this.strikeThrough, |
||||||
|
this.link, |
||||||
|
this.lists, |
||||||
|
this.quote, |
||||||
|
this.code); |
||||||
|
|
||||||
|
static DefaultStyles getInstance(BuildContext context) { |
||||||
|
ThemeData themeData = Theme.of(context); |
||||||
|
DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); |
||||||
|
TextStyle baseStyle = defaultTextStyle.style.copyWith( |
||||||
|
fontSize: 16.0, |
||||||
|
height: 1.3, |
||||||
|
); |
||||||
|
Tuple2 baseSpacing = Tuple2(6.0, 10); |
||||||
|
String fontFamily; |
||||||
|
switch (themeData.platform) { |
||||||
|
case TargetPlatform.iOS: |
||||||
|
case TargetPlatform.macOS: |
||||||
|
fontFamily = 'Menlo'; |
||||||
|
break; |
||||||
|
case TargetPlatform.android: |
||||||
|
case TargetPlatform.fuchsia: |
||||||
|
case TargetPlatform.windows: |
||||||
|
case TargetPlatform.linux: |
||||||
|
fontFamily = 'Roboto Mono'; |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw UnimplementedError(); |
||||||
|
} |
||||||
|
|
||||||
|
return DefaultStyles( |
||||||
|
DefaultTextBlockStyle( |
||||||
|
defaultTextStyle.style.copyWith( |
||||||
|
fontSize: 34.0, |
||||||
|
color: defaultTextStyle.style.color.withOpacity(0.70), |
||||||
|
height: 1.15, |
||||||
|
fontWeight: FontWeight.w300, |
||||||
|
), |
||||||
|
Tuple2(16.0, 0.0), |
||||||
|
Tuple2(0.0, 0.0), |
||||||
|
null), |
||||||
|
DefaultTextBlockStyle( |
||||||
|
defaultTextStyle.style.copyWith( |
||||||
|
fontSize: 24.0, |
||||||
|
color: defaultTextStyle.style.color.withOpacity(0.70), |
||||||
|
height: 1.15, |
||||||
|
fontWeight: FontWeight.normal, |
||||||
|
), |
||||||
|
Tuple2(8.0, 0.0), |
||||||
|
Tuple2(0.0, 0.0), |
||||||
|
null), |
||||||
|
DefaultTextBlockStyle( |
||||||
|
defaultTextStyle.style.copyWith( |
||||||
|
fontSize: 20.0, |
||||||
|
color: defaultTextStyle.style.color.withOpacity(0.70), |
||||||
|
height: 1.25, |
||||||
|
fontWeight: FontWeight.w500, |
||||||
|
), |
||||||
|
Tuple2(8.0, 0.0), |
||||||
|
Tuple2(0.0, 0.0), |
||||||
|
null), |
||||||
|
DefaultTextBlockStyle(baseStyle, baseSpacing, Tuple2(0.0, 0.0), null), |
||||||
|
TextStyle(fontWeight: FontWeight.bold), |
||||||
|
TextStyle(fontStyle: FontStyle.italic), |
||||||
|
TextStyle(decoration: TextDecoration.underline), |
||||||
|
TextStyle(decoration: TextDecoration.lineThrough), |
||||||
|
TextStyle( |
||||||
|
color: themeData.accentColor, |
||||||
|
decoration: TextDecoration.underline, |
||||||
|
), |
||||||
|
DefaultTextBlockStyle(baseStyle, baseSpacing, Tuple2(0.0, 6.0), null), |
||||||
|
DefaultTextBlockStyle( |
||||||
|
TextStyle(color: baseStyle.color.withOpacity(0.6)), |
||||||
|
baseSpacing, |
||||||
|
Tuple2(6.0, 2.0), |
||||||
|
BoxDecoration( |
||||||
|
border: Border( |
||||||
|
left: BorderSide(width: 4, color: Colors.grey.shade300), |
||||||
|
), |
||||||
|
)), |
||||||
|
DefaultTextBlockStyle( |
||||||
|
TextStyle( |
||||||
|
color: Colors.blue.shade900.withOpacity(0.9), |
||||||
|
fontFamily: fontFamily, |
||||||
|
fontSize: 13.0, |
||||||
|
height: 1.15, |
||||||
|
), |
||||||
|
baseSpacing, |
||||||
|
Tuple2(0.0, 0.0), |
||||||
|
BoxDecoration( |
||||||
|
color: Colors.grey.shade50, |
||||||
|
borderRadius: BorderRadius.circular(2), |
||||||
|
))); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,292 @@ |
|||||||
|
import 'package:flutter/rendering.dart'; |
||||||
|
import 'package:flutter/widgets.dart'; |
||||||
|
|
||||||
|
import 'box.dart'; |
||||||
|
|
||||||
|
class BaselineProxy extends SingleChildRenderObjectWidget { |
||||||
|
final TextStyle textStyle; |
||||||
|
final EdgeInsets padding; |
||||||
|
|
||||||
|
BaselineProxy({Key key, Widget child, this.textStyle, this.padding}) |
||||||
|
: super(key: key, child: child); |
||||||
|
|
||||||
|
@override |
||||||
|
RenderBaselineProxy createRenderObject(BuildContext context) { |
||||||
|
return RenderBaselineProxy( |
||||||
|
null, |
||||||
|
textStyle, |
||||||
|
padding, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void updateRenderObject( |
||||||
|
BuildContext context, covariant RenderBaselineProxy renderObject) { |
||||||
|
renderObject |
||||||
|
..textStyle = textStyle |
||||||
|
..padding = padding; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class RenderBaselineProxy extends RenderProxyBox { |
||||||
|
RenderBaselineProxy( |
||||||
|
RenderParagraph child, |
||||||
|
TextStyle textStyle, |
||||||
|
EdgeInsets padding, |
||||||
|
) : _prototypePainter = TextPainter( |
||||||
|
text: TextSpan(text: ' ', style: textStyle), |
||||||
|
textDirection: TextDirection.ltr, |
||||||
|
strutStyle: |
||||||
|
StrutStyle.fromTextStyle(textStyle, forceStrutHeight: true)), |
||||||
|
super(child); |
||||||
|
|
||||||
|
final TextPainter _prototypePainter; |
||||||
|
|
||||||
|
set textStyle(TextStyle value) { |
||||||
|
assert(value != null); |
||||||
|
if (_prototypePainter.text.style == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.text = TextSpan(text: ' ', style: value); |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
EdgeInsets _padding; |
||||||
|
|
||||||
|
set padding(EdgeInsets value) { |
||||||
|
assert(value != null); |
||||||
|
if (_padding == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_padding = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double computeDistanceToActualBaseline(TextBaseline baseline) => |
||||||
|
_prototypePainter.computeDistanceToActualBaseline(baseline) + |
||||||
|
_padding?.top ?? |
||||||
|
0.0; |
||||||
|
|
||||||
|
@override |
||||||
|
void performLayout() { |
||||||
|
super.performLayout(); |
||||||
|
_prototypePainter.layout(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class EmbedProxy extends SingleChildRenderObjectWidget { |
||||||
|
EmbedProxy(Widget child) : super(child: child); |
||||||
|
|
||||||
|
@override |
||||||
|
RenderEmbedProxy createRenderObject(BuildContext context) => |
||||||
|
RenderEmbedProxy(null); |
||||||
|
} |
||||||
|
|
||||||
|
class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { |
||||||
|
RenderEmbedProxy(RenderBox child) : super(child); |
||||||
|
|
||||||
|
@override |
||||||
|
List<TextBox> getBoxesForSelection(TextSelection selection) { |
||||||
|
if (!selection.isCollapsed) { |
||||||
|
return <TextBox>[ |
||||||
|
TextBox.fromLTRBD(0.0, 0.0, size.width, size.height, TextDirection.ltr) |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
double left = selection.extentOffset == 0 ? 0.0 : size.width; |
||||||
|
double right = selection.extentOffset == 0 ? 0.0 : size.width; |
||||||
|
return <TextBox>[ |
||||||
|
TextBox.fromLTRBD(left, 0.0, right, size.height, TextDirection.ltr) |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double getFullHeightForCaret(TextPosition position) => size.height; |
||||||
|
|
||||||
|
@override |
||||||
|
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { |
||||||
|
assert(position.offset != null && |
||||||
|
position.offset <= 1 && |
||||||
|
position.offset >= 0); |
||||||
|
return position.offset == 0 ? Offset.zero : Offset(size.width, 0.0); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextPosition getPositionForOffset(Offset offset) => |
||||||
|
TextPosition(offset: offset.dx > size.width / 2 ? 1 : 0); |
||||||
|
|
||||||
|
@override |
||||||
|
TextRange getWordBoundary(TextPosition position) => |
||||||
|
TextRange(start: 0, end: 1); |
||||||
|
|
||||||
|
@override |
||||||
|
double getPreferredLineHeight() { |
||||||
|
return size.height; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class RichTextProxy extends SingleChildRenderObjectWidget { |
||||||
|
final TextStyle textStyle; |
||||||
|
final TextDirection textDirection; |
||||||
|
final double textScaleFactor; |
||||||
|
final Locale locale; |
||||||
|
final StrutStyle strutStyle; |
||||||
|
final TextWidthBasis textWidthBasis; |
||||||
|
final TextHeightBehavior textHeightBehavior; |
||||||
|
|
||||||
|
@override |
||||||
|
RenderParagraphProxy createRenderObject(BuildContext context) { |
||||||
|
return RenderParagraphProxy(null, textStyle, textDirection, textScaleFactor, |
||||||
|
strutStyle, locale, textWidthBasis, textHeightBehavior); |
||||||
|
} |
||||||
|
|
||||||
|
RichTextProxy( |
||||||
|
RichText child, |
||||||
|
this.textStyle, |
||||||
|
this.textDirection, |
||||||
|
this.textScaleFactor, |
||||||
|
this.locale, |
||||||
|
this.strutStyle, |
||||||
|
this.textWidthBasis, |
||||||
|
this.textHeightBehavior) |
||||||
|
: assert(child != null), |
||||||
|
assert(textStyle != null), |
||||||
|
assert(textDirection != null), |
||||||
|
assert(locale != null), |
||||||
|
assert(strutStyle != null), |
||||||
|
super(child: child); |
||||||
|
|
||||||
|
@override |
||||||
|
void updateRenderObject( |
||||||
|
BuildContext context, covariant RenderParagraphProxy renderObject) { |
||||||
|
renderObject.textStyle = textStyle; |
||||||
|
renderObject.textDirection = textDirection; |
||||||
|
renderObject.textScaleFactor = textScaleFactor; |
||||||
|
renderObject.locale = locale; |
||||||
|
renderObject.strutStyle = strutStyle; |
||||||
|
renderObject.textWidthBasis = textWidthBasis; |
||||||
|
renderObject.textHeightBehavior = textHeightBehavior; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class RenderParagraphProxy extends RenderProxyBox |
||||||
|
implements RenderContentProxyBox { |
||||||
|
RenderParagraphProxy( |
||||||
|
RenderParagraph child, |
||||||
|
TextStyle textStyle, |
||||||
|
TextDirection textDirection, |
||||||
|
double textScaleFactor, |
||||||
|
StrutStyle strutStyle, |
||||||
|
Locale locale, |
||||||
|
TextWidthBasis textWidthBasis, |
||||||
|
TextHeightBehavior textHeightBehavior, |
||||||
|
) : _prototypePainter = TextPainter( |
||||||
|
text: TextSpan(text: ' ', style: textStyle), |
||||||
|
textAlign: TextAlign.left, |
||||||
|
textDirection: textDirection, |
||||||
|
textScaleFactor: textScaleFactor, |
||||||
|
strutStyle: strutStyle, |
||||||
|
locale: locale, |
||||||
|
textWidthBasis: textWidthBasis, |
||||||
|
textHeightBehavior: textHeightBehavior), |
||||||
|
super(child); |
||||||
|
|
||||||
|
final TextPainter _prototypePainter; |
||||||
|
|
||||||
|
set textStyle(TextStyle value) { |
||||||
|
assert(value != null); |
||||||
|
if (_prototypePainter.text.style == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.text = TextSpan(text: ' ', style: value); |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
set textDirection(TextDirection value) { |
||||||
|
assert(value != null); |
||||||
|
if (_prototypePainter.textDirection == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.textDirection = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
set textScaleFactor(double value) { |
||||||
|
assert(value != null); |
||||||
|
if (_prototypePainter.textScaleFactor == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.textScaleFactor = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
set strutStyle(StrutStyle value) { |
||||||
|
assert(value != null); |
||||||
|
if (_prototypePainter.strutStyle == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.strutStyle = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
set locale(Locale value) { |
||||||
|
if (_prototypePainter.locale == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.locale = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
set textWidthBasis(TextWidthBasis value) { |
||||||
|
assert(value != null); |
||||||
|
if (_prototypePainter.textWidthBasis == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.textWidthBasis = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
set textHeightBehavior(TextHeightBehavior value) { |
||||||
|
if (_prototypePainter.textHeightBehavior == value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_prototypePainter.textHeightBehavior = value; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
RenderParagraph get child => super.child; |
||||||
|
|
||||||
|
@override |
||||||
|
double getPreferredLineHeight() { |
||||||
|
return _prototypePainter.preferredLineHeight; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) => |
||||||
|
child.getOffsetForCaret(position, caretPrototype); |
||||||
|
|
||||||
|
@override |
||||||
|
TextPosition getPositionForOffset(Offset offset) => |
||||||
|
child.getPositionForOffset(offset); |
||||||
|
|
||||||
|
@override |
||||||
|
double getFullHeightForCaret(TextPosition position) => |
||||||
|
child.getFullHeightForCaret(position); |
||||||
|
|
||||||
|
@override |
||||||
|
TextRange getWordBoundary(TextPosition position) => |
||||||
|
child.getWordBoundary(position); |
||||||
|
|
||||||
|
@override |
||||||
|
List<TextBox> getBoxesForSelection(TextSelection selection) => |
||||||
|
child.getBoxesForSelection(selection); |
||||||
|
|
||||||
|
@override |
||||||
|
performLayout() { |
||||||
|
super.performLayout(); |
||||||
|
_prototypePainter.layout( |
||||||
|
minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_quill/models/documents/nodes/block.dart'; |
||||||
|
import 'package:tuple/tuple.dart'; |
||||||
|
|
||||||
|
import 'box.dart'; |
||||||
|
|
||||||
|
class EditableTextBlock extends StatelessWidget { |
||||||
|
final Block block; |
||||||
|
final TextDirection textDirection; |
||||||
|
final Tuple2 verticalSpacing; |
||||||
|
final TextSelection textSelection; |
||||||
|
final Color color; |
||||||
|
final bool enableInteractiveSelection; |
||||||
|
final bool hasFocus; |
||||||
|
final EdgeInsets contentPadding; |
||||||
|
|
||||||
|
EditableTextBlock( |
||||||
|
this.block, |
||||||
|
this.textDirection, |
||||||
|
this.verticalSpacing, |
||||||
|
this.textSelection, |
||||||
|
this.color, |
||||||
|
this.enableInteractiveSelection, |
||||||
|
this.hasFocus, |
||||||
|
this.contentPadding); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
// TODO: implement build |
||||||
|
throw UnimplementedError(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//class RenderEditableTextBlock extends RenderEditableContainerBox |
||||||
|
// implements RenderEditableBox { |
||||||
|
// |
||||||
|
// } |
@ -0,0 +1,552 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter/rendering.dart'; |
||||||
|
import 'package:flutter_quill/models/documents/attribute.dart'; |
||||||
|
import 'package:flutter_quill/models/documents/nodes/container.dart' |
||||||
|
as container; |
||||||
|
import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf; |
||||||
|
import 'package:flutter_quill/models/documents/nodes/line.dart'; |
||||||
|
import 'package:flutter_quill/models/documents/nodes/node.dart'; |
||||||
|
import 'package:flutter_quill/models/documents/style.dart'; |
||||||
|
import 'package:flutter_quill/widgets/cursor.dart'; |
||||||
|
import 'package:flutter_quill/widgets/proxy.dart'; |
||||||
|
import 'package:tuple/tuple.dart'; |
||||||
|
|
||||||
|
import 'box.dart'; |
||||||
|
import 'default_styles.dart'; |
||||||
|
|
||||||
|
class TextLine extends StatelessWidget { |
||||||
|
final Line line; |
||||||
|
final TextDirection textDirection; |
||||||
|
|
||||||
|
const TextLine({Key key, this.line, this.textDirection}) |
||||||
|
: assert(line != null), |
||||||
|
super(key: key); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
assert(debugCheckHasMediaQuery(context)); |
||||||
|
|
||||||
|
// TODO: line.hasEmbed |
||||||
|
|
||||||
|
TextSpan textSpan = _buildTextSpan(context); |
||||||
|
StrutStyle strutStyle = |
||||||
|
StrutStyle.fromTextStyle(textSpan.style, forceStrutHeight: true); |
||||||
|
return RichTextProxy( |
||||||
|
RichText( |
||||||
|
text: _buildTextSpan(context), |
||||||
|
textDirection: textDirection, |
||||||
|
strutStyle: strutStyle, |
||||||
|
textScaleFactor: MediaQuery.textScaleFactorOf(context), |
||||||
|
), |
||||||
|
textSpan.style, |
||||||
|
textDirection, |
||||||
|
1.0, |
||||||
|
Localizations.localeOf(context, nullOk: true), |
||||||
|
strutStyle, |
||||||
|
TextWidthBasis.parent, |
||||||
|
null); |
||||||
|
} |
||||||
|
|
||||||
|
TextSpan _buildTextSpan(BuildContext context) { |
||||||
|
DefaultStyles defaultStyles = DefaultStyles.getInstance(context); |
||||||
|
List<TextSpan> children = |
||||||
|
line.children.map((node) => _getTextSpanFromNode(defaultStyles, node)); |
||||||
|
|
||||||
|
TextStyle textStyle = TextStyle(); |
||||||
|
|
||||||
|
Attribute header = line.style.attributes[Attribute.header.key]; |
||||||
|
Map<Attribute, TextStyle> m = { |
||||||
|
Attribute.h1: defaultStyles.h1.style, |
||||||
|
Attribute.h2: defaultStyles.h2.style, |
||||||
|
Attribute.h3: defaultStyles.h3.style, |
||||||
|
}; |
||||||
|
|
||||||
|
textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph.style); |
||||||
|
|
||||||
|
Attribute block = line.style.getBlockExceptHeader(); |
||||||
|
TextStyle toMerge; |
||||||
|
if (block == Attribute.blockQuote) { |
||||||
|
toMerge = defaultStyles.quote.style; |
||||||
|
} else if (block == Attribute.codeBlock) { |
||||||
|
toMerge = defaultStyles.code.style; |
||||||
|
} else if (block != null) { |
||||||
|
toMerge = defaultStyles.lists.style; |
||||||
|
} |
||||||
|
|
||||||
|
textStyle = textStyle.merge(toMerge); |
||||||
|
|
||||||
|
return TextSpan(children: children, style: textStyle); |
||||||
|
} |
||||||
|
|
||||||
|
TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { |
||||||
|
leaf.Text textNode = node as leaf.Text; |
||||||
|
Style style = textNode.style; |
||||||
|
TextStyle res = TextStyle(); |
||||||
|
|
||||||
|
Map<Attribute, TextStyle> m = { |
||||||
|
Attribute.bold: defaultStyles.bold, |
||||||
|
Attribute.italic: defaultStyles.italic, |
||||||
|
Attribute.link: defaultStyles.link, |
||||||
|
Attribute.underline: defaultStyles.underline, |
||||||
|
Attribute.strikeThrough: defaultStyles.strikeThrough, |
||||||
|
}; |
||||||
|
m.forEach((k, s) { |
||||||
|
if (style.values.any((v) => v == k)) { |
||||||
|
res = _merge(res, s); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return TextSpan(text: textNode.value, style: res); |
||||||
|
} |
||||||
|
|
||||||
|
TextStyle _merge(TextStyle a, TextStyle b) { |
||||||
|
final decorations = <TextDecoration>[]; |
||||||
|
if (a.decoration != null) { |
||||||
|
decorations.add(a.decoration); |
||||||
|
} |
||||||
|
if (b.decoration != null) { |
||||||
|
decorations.add(b.decoration); |
||||||
|
} |
||||||
|
return a.merge(b).apply(decoration: TextDecoration.combine(decorations)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class EditableTextLine extends RenderObjectWidget { |
||||||
|
final Line line; |
||||||
|
final Widget leading; |
||||||
|
final Widget body; |
||||||
|
final double indentWidth; |
||||||
|
final Tuple2 verticalSpacing; |
||||||
|
final TextDirection textDirection; |
||||||
|
final TextSelection textSelection; |
||||||
|
final Color color; |
||||||
|
final bool enableInteractiveSelection; |
||||||
|
final bool hasFocus; |
||||||
|
final double devicePixelRatio; |
||||||
|
final CursorCont cursorCont; |
||||||
|
|
||||||
|
EditableTextLine( |
||||||
|
this.line, |
||||||
|
this.leading, |
||||||
|
this.body, |
||||||
|
this.indentWidth, |
||||||
|
this.verticalSpacing, |
||||||
|
this.textDirection, |
||||||
|
this.textSelection, |
||||||
|
this.color, |
||||||
|
this.enableInteractiveSelection, |
||||||
|
this.hasFocus, |
||||||
|
this.devicePixelRatio, |
||||||
|
this.cursorCont) |
||||||
|
: assert(line != null), |
||||||
|
assert(indentWidth != null), |
||||||
|
assert(textSelection != null), |
||||||
|
assert(color != null), |
||||||
|
assert(enableInteractiveSelection != null), |
||||||
|
assert(hasFocus != null), |
||||||
|
assert(cursorCont != null); |
||||||
|
|
||||||
|
@override |
||||||
|
RenderObjectElement createElement() { |
||||||
|
return _TextLineElement(this); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
RenderObject createRenderObject(BuildContext context) { |
||||||
|
return RenderEditableTextLine( |
||||||
|
line, |
||||||
|
textDirection, |
||||||
|
textSelection, |
||||||
|
enableInteractiveSelection, |
||||||
|
hasFocus, |
||||||
|
devicePixelRatio, |
||||||
|
_getPadding(), |
||||||
|
cursorCont); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
updateRenderObject( |
||||||
|
BuildContext context, covariant RenderEditableTextLine renderObject) { |
||||||
|
renderObject.setLine(line); |
||||||
|
renderObject.setPadding(_getPadding()); |
||||||
|
renderObject.setTextDirection(textDirection); |
||||||
|
renderObject.setTextSelection(textSelection); |
||||||
|
renderObject.setColor(color); |
||||||
|
renderObject.setEnableInteractiveSelection(enableInteractiveSelection); |
||||||
|
renderObject.hasFocus = hasFocus; |
||||||
|
renderObject.setDevicePixelRatio(devicePixelRatio); |
||||||
|
renderObject.setCursorCont(cursorCont); |
||||||
|
} |
||||||
|
|
||||||
|
EdgeInsetsGeometry _getPadding() { |
||||||
|
return EdgeInsetsDirectional.only( |
||||||
|
start: indentWidth, |
||||||
|
top: verticalSpacing.item1, |
||||||
|
bottom: verticalSpacing.item2); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
enum TextLineSlot { LEADING, BODY } |
||||||
|
|
||||||
|
class RenderEditableTextLine extends RenderEditableBox { |
||||||
|
RenderBox leading; |
||||||
|
RenderContentProxyBox body; |
||||||
|
Line line; |
||||||
|
TextDirection textDirection; |
||||||
|
TextSelection textSelection; |
||||||
|
Color color; |
||||||
|
bool enableInteractiveSelection; |
||||||
|
bool hasFocus = false; |
||||||
|
double devicePixelRatio; |
||||||
|
EdgeInsetsGeometry padding; |
||||||
|
CursorCont cursorCont; |
||||||
|
EdgeInsets _resolvedPadding; |
||||||
|
bool _containsCursor; |
||||||
|
List<TextBox> _selectedRects; |
||||||
|
Rect _caretPrototype; |
||||||
|
final Map<TextLineSlot, RenderBox> children = <TextLineSlot, RenderBox>{}; |
||||||
|
|
||||||
|
RenderEditableTextLine( |
||||||
|
this.line, |
||||||
|
this.textDirection, |
||||||
|
this.textSelection, |
||||||
|
this.enableInteractiveSelection, |
||||||
|
this.hasFocus, |
||||||
|
this.devicePixelRatio, |
||||||
|
this.padding, |
||||||
|
this.cursorCont) |
||||||
|
: assert(line != null), |
||||||
|
assert(padding != null), |
||||||
|
assert(padding.isNonNegative), |
||||||
|
assert(devicePixelRatio != null), |
||||||
|
assert(hasFocus != null), |
||||||
|
assert(cursorCont != null); |
||||||
|
|
||||||
|
setCursorCont(CursorCont c) { |
||||||
|
assert(c != null); |
||||||
|
if (cursorCont == c) { |
||||||
|
return; |
||||||
|
} |
||||||
|
cursorCont = c; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
setDevicePixelRatio(double d) { |
||||||
|
if (devicePixelRatio == d) { |
||||||
|
return; |
||||||
|
} |
||||||
|
devicePixelRatio = d; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
setEnableInteractiveSelection(bool val) { |
||||||
|
if (enableInteractiveSelection == val) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
markNeedsLayout(); |
||||||
|
markNeedsSemanticsUpdate(); |
||||||
|
} |
||||||
|
|
||||||
|
setColor(Color c) { |
||||||
|
if (color == c) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
color = c; |
||||||
|
if (containsTextSelection()) { |
||||||
|
markNeedsPaint(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setTextSelection(TextSelection t) { |
||||||
|
if (textSelection == t) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
bool containsSelection = containsTextSelection(); |
||||||
|
if (attached && containsCursor()) { |
||||||
|
cursorCont.removeListener(markNeedsLayout); |
||||||
|
cursorCont.color.removeListener(markNeedsPaint); |
||||||
|
} |
||||||
|
|
||||||
|
textSelection = t; |
||||||
|
_selectedRects = null; |
||||||
|
_containsCursor = null; |
||||||
|
if (attached && containsCursor()) { |
||||||
|
cursorCont.addListener(markNeedsLayout); |
||||||
|
cursorCont.color.addListener(markNeedsPaint); |
||||||
|
} |
||||||
|
|
||||||
|
if (containsSelection || containsTextSelection()) { |
||||||
|
markNeedsPaint(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setTextDirection(TextDirection t) { |
||||||
|
if (textDirection == t) { |
||||||
|
return; |
||||||
|
} |
||||||
|
textDirection = t; |
||||||
|
_resolvedPadding = null; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
setLine(Line l) { |
||||||
|
assert(l != null); |
||||||
|
if (line == l) { |
||||||
|
return; |
||||||
|
} |
||||||
|
line = l; |
||||||
|
_containsCursor = null; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
setPadding(EdgeInsetsGeometry p) { |
||||||
|
assert(p != null); |
||||||
|
assert(p.isNonNegative); |
||||||
|
if (padding == p) { |
||||||
|
return; |
||||||
|
} |
||||||
|
padding = p; |
||||||
|
_resolvedPadding = null; |
||||||
|
markNeedsLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
setLeading(RenderBox l) { |
||||||
|
leading = _updateChild(leading, l, TextLineSlot.LEADING); |
||||||
|
} |
||||||
|
|
||||||
|
setBody(RenderContentProxyBox b) { |
||||||
|
body = _updateChild(body, b, TextLineSlot.BODY); |
||||||
|
} |
||||||
|
|
||||||
|
bool containsTextSelection() { |
||||||
|
return line.getDocumentOffset() <= textSelection.end && |
||||||
|
textSelection.start <= line.getDocumentOffset() + line.length - 1; |
||||||
|
} |
||||||
|
|
||||||
|
bool containsCursor() { |
||||||
|
return _containsCursor ??= textSelection.isCollapsed && |
||||||
|
line.containsOffset(textSelection.baseOffset); |
||||||
|
} |
||||||
|
|
||||||
|
RenderBox _updateChild(RenderBox old, RenderBox newChild, TextLineSlot slot) { |
||||||
|
if (old != null) { |
||||||
|
dropChild(old); |
||||||
|
children.remove(slot); |
||||||
|
} |
||||||
|
if (newChild != null) { |
||||||
|
children[slot] = newChild; |
||||||
|
adoptChild(newChild); |
||||||
|
} |
||||||
|
return newChild; |
||||||
|
} |
||||||
|
|
||||||
|
List<TextBox> _getBoxes(TextSelection textSelection) { |
||||||
|
BoxParentData parentData = body.parentData as BoxParentData; |
||||||
|
return body.getBoxesForSelection(textSelection).map((box) { |
||||||
|
return TextBox.fromLTRBD( |
||||||
|
box.left + parentData.offset.dx, |
||||||
|
box.top + parentData.offset.dy, |
||||||
|
box.right + parentData.offset.dx, |
||||||
|
box.bottom + parentData.offset.dy, |
||||||
|
box.direction, |
||||||
|
); |
||||||
|
}).toList(growable: false); |
||||||
|
} |
||||||
|
|
||||||
|
_resolvePadding() { |
||||||
|
if (_resolvedPadding != null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_resolvedPadding = padding.resolve(textDirection); |
||||||
|
assert(_resolvedPadding.isNonNegative); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection) { |
||||||
|
return _getEndpointForSelection(textSelection, true); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextSelectionPoint getExtentEndpointForSelection( |
||||||
|
TextSelection textSelection) { |
||||||
|
return _getEndpointForSelection(textSelection, false); |
||||||
|
} |
||||||
|
|
||||||
|
TextSelectionPoint _getEndpointForSelection( |
||||||
|
TextSelection textSelection, bool first) { |
||||||
|
if (textSelection.isCollapsed) { |
||||||
|
return TextSelectionPoint( |
||||||
|
Offset(0.0, preferredLineHeight(textSelection.extent)) + |
||||||
|
getOffsetForCaret(textSelection.extent), |
||||||
|
null); |
||||||
|
} |
||||||
|
List<TextBox> boxes = _getBoxes(textSelection); |
||||||
|
assert(boxes.isNotEmpty); |
||||||
|
TextBox targetBox = first ? boxes.first : boxes.last; |
||||||
|
return TextSelectionPoint( |
||||||
|
Offset(targetBox.start, targetBox.bottom), targetBox.direction); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextRange getLineBoundary(TextPosition position) { |
||||||
|
double lineDy = getOffsetForCaret(position) |
||||||
|
.translate(0.0, 0.5 * preferredLineHeight(position)) |
||||||
|
.dy; |
||||||
|
List<TextBox> lineBoxes = |
||||||
|
_getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1)) |
||||||
|
.where((element) => element.top < lineDy && element.bottom > lineDy) |
||||||
|
.toList(growable: false); |
||||||
|
return TextRange( |
||||||
|
start: |
||||||
|
getPositionForOffset(Offset(lineBoxes.first.left, lineDy)).offset, |
||||||
|
end: getPositionForOffset(Offset(lineBoxes.last.right, lineDy)).offset); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Offset getOffsetForCaret(TextPosition position) { |
||||||
|
return body.getOffsetForCaret(position, _caretPrototype) + |
||||||
|
(body.parentData as BoxParentData).offset; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextPosition getPositionAbove(TextPosition position) { |
||||||
|
return _getPosition(position, -0.5); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextPosition getPositionBelow(TextPosition position) { |
||||||
|
return _getPosition(position, 1.5); |
||||||
|
} |
||||||
|
|
||||||
|
TextPosition _getPosition(TextPosition textPosition, double dyScale) { |
||||||
|
assert(textPosition.offset < line.length); |
||||||
|
Offset offset = getOffsetForCaret(textPosition) |
||||||
|
.translate(0, dyScale * preferredLineHeight(textPosition)); |
||||||
|
if (body.size |
||||||
|
.contains(offset - (body.parentData as BoxParentData).offset)) { |
||||||
|
return getPositionForOffset(offset); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextPosition getPositionForOffset(Offset offset) { |
||||||
|
return body.getPositionForOffset( |
||||||
|
offset - (body.parentData as BoxParentData).offset); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
TextRange getWordBoundary(TextPosition position) { |
||||||
|
return body.getWordBoundary(position); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
double preferredLineHeight(TextPosition position) { |
||||||
|
return body.getPreferredLineHeight(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
container.Container getContainer() { |
||||||
|
return line; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class _TextLineElement extends RenderObjectElement { |
||||||
|
_TextLineElement(EditableTextLine line) : super(line); |
||||||
|
|
||||||
|
final Map<TextLineSlot, Element> _slotToChildren = <TextLineSlot, Element>{}; |
||||||
|
|
||||||
|
@override |
||||||
|
EditableTextLine get widget => super.widget as EditableTextLine; |
||||||
|
|
||||||
|
@override |
||||||
|
RenderEditableTextLine get renderObject => |
||||||
|
super.renderObject as RenderEditableTextLine; |
||||||
|
|
||||||
|
@override |
||||||
|
visitChildren(ElementVisitor visitor) { |
||||||
|
_slotToChildren.values.forEach(visitor); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
forgetChild(Element child) { |
||||||
|
assert(_slotToChildren.containsValue(child)); |
||||||
|
assert(child.slot is TextLineSlot); |
||||||
|
assert(_slotToChildren.containsKey(child.slot)); |
||||||
|
_slotToChildren.remove(child.slot); |
||||||
|
super.forgetChild(child); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
mount(Element parent, dynamic newSlot) { |
||||||
|
super.mount(parent, newSlot); |
||||||
|
_mountChild(widget.leading, TextLineSlot.LEADING); |
||||||
|
_mountChild(widget.body, TextLineSlot.BODY); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
update(EditableTextLine newWidget) { |
||||||
|
super.update(newWidget); |
||||||
|
assert(widget == newWidget); |
||||||
|
_updateChild(widget.leading, TextLineSlot.LEADING); |
||||||
|
_updateChild(widget.body, TextLineSlot.BODY); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
insertRenderObjectChild(RenderObject child, TextLineSlot slot) { |
||||||
|
assert(child is RenderBox); |
||||||
|
_updateRenderObject(child, slot); |
||||||
|
assert(renderObject.children.keys.contains(slot)); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
removeRenderObjectChild(RenderObject child, TextLineSlot slot) { |
||||||
|
assert(child is RenderBox); |
||||||
|
assert(renderObject.children[slot] == child); |
||||||
|
_updateRenderObject(null, slot); |
||||||
|
assert(!renderObject.children.keys.contains(slot)); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
moveRenderObjectChild(RenderObject child, dynamic oldSlot, dynamic newSlot) { |
||||||
|
throw UnimplementedError(); |
||||||
|
} |
||||||
|
|
||||||
|
_mountChild(Widget widget, TextLineSlot slot) { |
||||||
|
Element oldChild = _slotToChildren[slot]; |
||||||
|
Element newChild = updateChild(oldChild, widget, slot); |
||||||
|
if (oldChild != null) { |
||||||
|
_slotToChildren.remove(slot); |
||||||
|
} |
||||||
|
if (newChild != null) { |
||||||
|
_slotToChildren[slot] = newChild; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_updateRenderObject(RenderObject child, TextLineSlot slot) { |
||||||
|
switch (slot) { |
||||||
|
case TextLineSlot.LEADING: |
||||||
|
renderObject.leading = child as RenderBox; |
||||||
|
break; |
||||||
|
case TextLineSlot.BODY: |
||||||
|
renderObject.body = child as RenderBox; |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw UnimplementedError(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_updateChild(Widget widget, TextLineSlot slot) { |
||||||
|
Element oldChild = _slotToChildren[slot]; |
||||||
|
Element newChild = updateChild(oldChild, widget, slot); |
||||||
|
if (oldChild != null) { |
||||||
|
_slotToChildren.remove(slot); |
||||||
|
} |
||||||
|
if (newChild != null) { |
||||||
|
_slotToChildren[slot] = newChild; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue