Add default styles and text line

pull/13/head
singerdmx 4 years ago
parent dc7b4e794f
commit 37fc173dde
  1. 8
      lib/models/documents/nodes/line.dart
  2. 24
      lib/models/documents/nodes/node.dart
  3. 2
      lib/models/documents/style.dart
  4. 38
      lib/widgets/box.dart
  5. 105
      lib/widgets/cursor.dart
  6. 134
      lib/widgets/default_styles.dart
  7. 292
      lib/widgets/proxy.dart
  8. 37
      lib/widgets/text_block.dart
  9. 552
      lib/widgets/text_line.dart

@ -17,6 +17,14 @@ class Line extends Container<Leaf> {
@override
int get length => super.length + 1;
bool get hasEmbed {
if (childCount != 1) {
return false;
}
return children.single is Embed;
}
Line get nextLine {
if (!isLast) {
return next is Block ? (next as Block).first : next;

@ -41,6 +41,30 @@ abstract class Node extends LinkedListEntry<Node> {
return node;
}
int getOffset() {
int offset = 0;
if (isFirst) {
return offset;
}
Node cur = this;
do {
cur = cur.previous;
offset += cur.length;
} while (!cur.isFirst);
return offset;
}
int getDocumentOffset() {
return parent is! Root ? parent.getDocumentOffset() : 0 + getOffset();
}
bool containsOffset(int offset) {
return getDocumentOffset() <= offset &&
offset < getDocumentOffset() + this.length;
}
@override
void insertBefore(Node entry) {
assert(entry.parent == null && parent != null);

@ -29,6 +29,8 @@ class Style {
Iterable<Attribute> get values => _attributes.values;
Map<String, Attribute> get attributes => _attributes;
bool get isEmpty => _attributes.isEmpty;
bool get isNotEmpty => _attributes.isNotEmpty;

@ -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);
}

@ -1,5 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'box.dart';
const Duration _FADE_DURATION = Duration(milliseconds: 250);
class CursorStyle {
@ -29,16 +32,16 @@ class CursorStyle {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CursorStyle &&
runtimeType == other.runtimeType &&
color == other.color &&
backgroundColor == other.backgroundColor &&
width == other.width &&
height == other.height &&
radius == other.radius &&
offset == other.offset &&
opacityAnimates == other.opacityAnimates &&
paintAboveText == other.paintAboveText;
other is CursorStyle &&
runtimeType == other.runtimeType &&
color == other.color &&
backgroundColor == other.backgroundColor &&
width == other.width &&
height == other.height &&
radius == other.radius &&
offset == other.offset &&
opacityAnimates == other.opacityAnimates &&
paintAboveText == other.paintAboveText;
@override
int get hashCode =>
@ -53,10 +56,9 @@ class CursorStyle {
}
class CursorCont extends ChangeNotifier {
final ValueNotifier<bool> show;
final ValueNotifier<bool> _blink;
final ValueNotifier<Color> _color;
final ValueNotifier<Color> color;
AnimationController _blinkOpacityCont;
CursorStyle _style;
@ -70,14 +72,89 @@ class CursorCont extends ChangeNotifier {
show = show ?? ValueNotifier<bool>(false),
_style = style,
_blink = ValueNotifier(false),
_color = ValueNotifier(style.color) {
color = ValueNotifier(style.color) {
_blinkOpacityCont =
AnimationController(vsync: tickerProvider, duration: _FADE_DURATION);
_blinkOpacityCont.addListener(_onColorTick);
}
void _onColorTick() {
_color.value = _style.color.withOpacity(_blinkOpacityCont.value);
color.value = _style.color.withOpacity(_blinkOpacityCont.value);
_blink.value = show.value && _blinkOpacityCont.value > 0;
}
}
class CursorPainter {
final RenderContentProxyBox editable;
final CursorStyle style;
final Rect prototype;
final Color color;
final double devicePixelRatio;
CursorPainter(this.editable, this.style, this.prototype, this.color,
this.devicePixelRatio);
paint(Canvas canvas, Offset offset, TextPosition position) {
assert(prototype != null);
Offset caretOffset =
editable.getOffsetForCaret(position, prototype) + offset;
Rect caretRect = prototype.shift(caretOffset);
if (style.offset != null) {
caretRect = caretRect.shift(style.offset);
}
if (caretRect.left < 0.0) {
caretRect = caretRect.shift(Offset(-caretRect.left, 0.0));
}
double caretHeight = editable.getFullHeightForCaret(position);
if (caretHeight != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top - 2.0,
caretRect.width,
caretHeight,
);
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top + (caretHeight - caretRect.height) / 2,
caretRect.width,
caretRect.height,
);
break;
default:
throw UnimplementedError();
}
}
Offset caretPosition = editable.localToGlobal(caretRect.topLeft);
double pixelMultiple = 1.0 / devicePixelRatio;
caretRect = caretRect.shift(Offset(
caretPosition.dx.isFinite
? (caretPosition.dx / pixelMultiple).round() * pixelMultiple -
caretPosition.dx
: caretPosition.dx,
caretPosition.dy.isFinite
? (caretPosition.dy / pixelMultiple).round() * pixelMultiple -
caretPosition.dy
: caretPosition.dy));
Paint paint = Paint()..color = color;
if (style.radius == null) {
canvas.drawRect(caretRect, paint);
return;
}
RRect caretRRect = RRect.fromRectAndRadius(caretRect, style.radius);
canvas.drawRRect(caretRRect, paint);
}
}

@ -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…
Cancel
Save