|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
|
|
|
|
import '../models/documents/attribute.dart';
|
|
|
|
import '../models/documents/nodes/container.dart' as container;
|
|
|
|
import '../models/documents/nodes/leaf.dart' as leaf;
|
|
|
|
import '../models/documents/nodes/leaf.dart';
|
|
|
|
import '../models/documents/nodes/line.dart';
|
|
|
|
import '../models/documents/nodes/node.dart';
|
|
|
|
import '../utils/color.dart';
|
|
|
|
import 'box.dart';
|
|
|
|
import 'cursor.dart';
|
|
|
|
import 'default_styles.dart';
|
|
|
|
import 'delegate.dart';
|
|
|
|
import 'proxy.dart';
|
|
|
|
import 'text_selection.dart';
|
|
|
|
|
|
|
|
class TextLine extends StatelessWidget {
|
|
|
|
const TextLine({
|
|
|
|
required this.line,
|
|
|
|
required this.embedBuilder,
|
|
|
|
required this.styles,
|
|
|
|
this.textDirection,
|
|
|
|
Key? key,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
final Line line;
|
|
|
|
final TextDirection? textDirection;
|
|
|
|
final EmbedBuilder embedBuilder;
|
|
|
|
final DefaultStyles styles;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
assert(debugCheckHasMediaQuery(context));
|
|
|
|
|
|
|
|
if (line.hasEmbed) {
|
|
|
|
final embed = line.children.single as Embed;
|
|
|
|
return EmbedProxy(embedBuilder(context, embed));
|
|
|
|
}
|
|
|
|
|
|
|
|
final textSpan = _buildTextSpan(context);
|
|
|
|
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
|
|
|
|
final textAlign = _getTextAlign();
|
|
|
|
final child = RichText(
|
|
|
|
text: textSpan,
|
|
|
|
textAlign: textAlign,
|
|
|
|
textDirection: textDirection,
|
|
|
|
strutStyle: strutStyle,
|
|
|
|
textScaleFactor: MediaQuery.textScaleFactorOf(context),
|
|
|
|
);
|
|
|
|
return RichTextProxy(
|
|
|
|
child,
|
|
|
|
textSpan.style!,
|
|
|
|
textAlign,
|
|
|
|
textDirection!,
|
|
|
|
1,
|
|
|
|
Localizations.localeOf(context),
|
|
|
|
strutStyle,
|
|
|
|
TextWidthBasis.parent,
|
|
|
|
null);
|
|
|
|
}
|
|
|
|
|
|
|
|
TextAlign _getTextAlign() {
|
|
|
|
final alignment = line.style.attributes[Attribute.align.key];
|
|
|
|
if (alignment == Attribute.leftAlignment) {
|
|
|
|
return TextAlign.left;
|
|
|
|
} else if (alignment == Attribute.centerAlignment) {
|
|
|
|
return TextAlign.center;
|
|
|
|
} else if (alignment == Attribute.rightAlignment) {
|
|
|
|
return TextAlign.right;
|
|
|
|
} else if (alignment == Attribute.justifyAlignment) {
|
|
|
|
return TextAlign.justify;
|
|
|
|
}
|
|
|
|
return TextAlign.start;
|
|
|
|
}
|
|
|
|
|
|
|
|
TextSpan _buildTextSpan(BuildContext context) {
|
|
|
|
final defaultStyles = styles;
|
|
|
|
final children = line.children
|
|
|
|
.map((node) => _getTextSpanFromNode(defaultStyles, node))
|
|
|
|
.toList(growable: false);
|
|
|
|
|
|
|
|
var textStyle = const TextStyle();
|
|
|
|
|
|
|
|
if (line.style.containsKey(Attribute.placeholder.key)) {
|
|
|
|
textStyle = defaultStyles.placeHolder!.style;
|
|
|
|
return TextSpan(children: children, style: textStyle);
|
|
|
|
}
|
|
|
|
|
|
|
|
final header = line.style.attributes[Attribute.header.key];
|
|
|
|
final m = <Attribute, TextStyle>{
|
|
|
|
Attribute.h1: defaultStyles.h1!.style,
|
|
|
|
Attribute.h2: defaultStyles.h2!.style,
|
|
|
|
Attribute.h3: defaultStyles.h3!.style,
|
|
|
|
};
|
|
|
|
|
|
|
|
textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style);
|
|
|
|
|
|
|
|
final 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) {
|
|
|
|
final textNode = node as leaf.Text;
|
|
|
|
final style = textNode.style;
|
|
|
|
var res = const TextStyle();
|
|
|
|
|
|
|
|
<String, TextStyle?>{
|
|
|
|
Attribute.bold.key: defaultStyles.bold,
|
|
|
|
Attribute.italic.key: defaultStyles.italic,
|
|
|
|
Attribute.link.key: defaultStyles.link,
|
|
|
|
Attribute.underline.key: defaultStyles.underline,
|
|
|
|
Attribute.strikeThrough.key: defaultStyles.strikeThrough,
|
|
|
|
}.forEach((k, s) {
|
|
|
|
if (style.values.any((v) => v.key == k)) {
|
|
|
|
res = _merge(res, s!);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
final font = textNode.style.attributes[Attribute.font.key];
|
|
|
|
if (font != null && font.value != null) {
|
|
|
|
res = res.merge(TextStyle(fontFamily: font.value));
|
|
|
|
}
|
|
|
|
|
|
|
|
final size = textNode.style.attributes[Attribute.size.key];
|
|
|
|
if (size != null && size.value != null) {
|
|
|
|
switch (size.value) {
|
|
|
|
case 'small':
|
|
|
|
res = res.merge(defaultStyles.sizeSmall);
|
|
|
|
break;
|
|
|
|
case 'large':
|
|
|
|
res = res.merge(defaultStyles.sizeLarge);
|
|
|
|
break;
|
|
|
|
case 'huge':
|
|
|
|
res = res.merge(defaultStyles.sizeHuge);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
final fontSize = double.tryParse(size.value);
|
|
|
|
if (fontSize != null) {
|
|
|
|
res = res.merge(TextStyle(fontSize: fontSize));
|
|
|
|
} else {
|
|
|
|
throw 'Invalid size ${size.value}';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final color = textNode.style.attributes[Attribute.color.key];
|
|
|
|
if (color != null && color.value != null) {
|
|
|
|
var textColor = defaultStyles.color;
|
|
|
|
if (color.value is String) {
|
|
|
|
textColor = stringToColor(color.value);
|
|
|
|
}
|
|
|
|
if (textColor != null) {
|
|
|
|
res = res.merge(TextStyle(color: textColor));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final background = textNode.style.attributes[Attribute.background.key];
|
|
|
|
if (background != null && background.value != null) {
|
|
|
|
final backgroundColor = stringToColor(background.value);
|
|
|
|
res = res.merge(TextStyle(backgroundColor: backgroundColor));
|
|
|
|
}
|
|
|
|
|
|
|
|
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(
|
|
|
|
List.castFrom<dynamic, TextDecoration>(decorations)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class EditableTextLine extends RenderObjectWidget {
|
|
|
|
const 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,
|
|
|
|
);
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
@override
|
|
|
|
RenderObjectElement createElement() {
|
|
|
|
return _TextLineElement(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
|
|
return RenderEditableTextLine(
|
|
|
|
line,
|
|
|
|
textDirection,
|
|
|
|
textSelection,
|
|
|
|
enableInteractiveSelection,
|
|
|
|
hasFocus,
|
|
|
|
devicePixelRatio,
|
|
|
|
_getPadding(),
|
|
|
|
color,
|
|
|
|
cursorCont);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void updateRenderObject(
|
|
|
|
BuildContext context, covariant RenderEditableTextLine renderObject) {
|
|
|
|
renderObject
|
|
|
|
..setLine(line)
|
|
|
|
..setPadding(_getPadding())
|
|
|
|
..setTextDirection(textDirection)
|
|
|
|
..setTextSelection(textSelection)
|
|
|
|
..setColor(color)
|
|
|
|
..setEnableInteractiveSelection(enableInteractiveSelection)
|
|
|
|
..hasFocus = hasFocus
|
|
|
|
..setDevicePixelRatio(devicePixelRatio)
|
|
|
|
..setCursorCont(cursorCont);
|
|
|
|
}
|
|
|
|
|
|
|
|
EdgeInsetsGeometry _getPadding() {
|
|
|
|
return EdgeInsetsDirectional.only(
|
|
|
|
start: indentWidth,
|
|
|
|
top: verticalSpacing.item1,
|
|
|
|
bottom: verticalSpacing.item2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum TextLineSlot { LEADING, BODY }
|
|
|
|
|
|
|
|
class RenderEditableTextLine extends RenderEditableBox {
|
|
|
|
RenderEditableTextLine(
|
|
|
|
this.line,
|
|
|
|
this.textDirection,
|
|
|
|
this.textSelection,
|
|
|
|
this.enableInteractiveSelection,
|
|
|
|
this.hasFocus,
|
|
|
|
this.devicePixelRatio,
|
|
|
|
this.padding,
|
|
|
|
this.color,
|
|
|
|
this.cursorCont,
|
|
|
|
);
|
|
|
|
|
|
|
|
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>{};
|
|
|
|
|
|
|
|
Iterable<RenderBox> get _children sync* {
|
|
|
|
if (_leading != null) {
|
|
|
|
yield _leading!;
|
|
|
|
}
|
|
|
|
if (_body != null) {
|
|
|
|
yield _body!;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setCursorCont(CursorCont c) {
|
|
|
|
if (cursorCont == c) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
cursorCont = c;
|
|
|
|
markNeedsLayout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void setDevicePixelRatio(double d) {
|
|
|
|
if (devicePixelRatio == d) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
devicePixelRatio = d;
|
|
|
|
markNeedsLayout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void setEnableInteractiveSelection(bool val) {
|
|
|
|
if (enableInteractiveSelection == val) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
markNeedsLayout();
|
|
|
|
markNeedsSemanticsUpdate();
|
|
|
|
}
|
|
|
|
|
|
|
|
void setColor(Color c) {
|
|
|
|
if (color == c) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
color = c;
|
|
|
|
if (containsTextSelection()) {
|
|
|
|
markNeedsPaint();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setTextSelection(TextSelection t) {
|
|
|
|
if (textSelection == t) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void setTextDirection(TextDirection t) {
|
|
|
|
if (textDirection == t) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
textDirection = t;
|
|
|
|
_resolvedPadding = null;
|
|
|
|
markNeedsLayout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void setLine(Line l) {
|
|
|
|
if (line == l) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
line = l;
|
|
|
|
_containsCursor = null;
|
|
|
|
markNeedsLayout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void setPadding(EdgeInsetsGeometry p) {
|
|
|
|
assert(p.isNonNegative);
|
|
|
|
if (padding == p) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
padding = p;
|
|
|
|
_resolvedPadding = null;
|
|
|
|
markNeedsLayout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void setLeading(RenderBox? l) {
|
|
|
|
_leading = _updateChild(_leading, l, TextLineSlot.LEADING);
|
|
|
|
}
|
|
|
|
|
|
|
|
void setBody(RenderContentProxyBox? b) {
|
|
|
|
_body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool containsTextSelection() {
|
|
|
|
return line.documentOffset <= textSelection.end &&
|
|
|
|
textSelection.start <= line.documentOffset + 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) {
|
|
|
|
final 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
void _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, preferredLineHeight(textSelection.extent)) +
|
|
|
|
getOffsetForCaret(textSelection.extent),
|
|
|
|
null);
|
|
|
|
}
|
|
|
|
final boxes = _getBoxes(textSelection);
|
|
|
|
assert(boxes.isNotEmpty);
|
|
|
|
final targetBox = first ? boxes.first : boxes.last;
|
|
|
|
return TextSelectionPoint(
|
|
|
|
Offset(first ? targetBox.start : targetBox.end, targetBox.bottom),
|
|
|
|
targetBox.direction);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
TextRange getLineBoundary(TextPosition position) {
|
|
|
|
final lineDy = getOffsetForCaret(position)
|
|
|
|
.translate(0, 0.5 * preferredLineHeight(position))
|
|
|
|
.dy;
|
|
|
|
final 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);
|
|
|
|
final 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
double get cursorWidth => cursorCont.style.width;
|
|
|
|
|
|
|
|
double get cursorHeight =>
|
|
|
|
cursorCont.style.height ??
|
|
|
|
preferredLineHeight(const TextPosition(offset: 0));
|
|
|
|
|
|
|
|
void _computeCaretPrototype() {
|
|
|
|
switch (defaultTargetPlatform) {
|
|
|
|
case TargetPlatform.iOS:
|
|
|
|
case TargetPlatform.macOS:
|
|
|
|
_caretPrototype = Rect.fromLTWH(0, 0, cursorWidth, cursorHeight + 2);
|
|
|
|
break;
|
|
|
|
case TargetPlatform.android:
|
|
|
|
case TargetPlatform.fuchsia:
|
|
|
|
case TargetPlatform.linux:
|
|
|
|
case TargetPlatform.windows:
|
|
|
|
_caretPrototype = Rect.fromLTWH(0, 2, cursorWidth, cursorHeight - 4.0);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw 'Invalid platform';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void attach(covariant PipelineOwner owner) {
|
|
|
|
super.attach(owner);
|
|
|
|
for (final child in _children) {
|
|
|
|
child.attach(owner);
|
|
|
|
}
|
|
|
|
if (containsCursor()) {
|
|
|
|
cursorCont.addListener(markNeedsLayout);
|
|
|
|
cursorCont.cursorColor.addListener(markNeedsPaint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void detach() {
|
|
|
|
super.detach();
|
|
|
|
for (final child in _children) {
|
|
|
|
child.detach();
|
|
|
|
}
|
|
|
|
if (containsCursor()) {
|
|
|
|
cursorCont.removeListener(markNeedsLayout);
|
|
|
|
cursorCont.cursorColor.removeListener(markNeedsPaint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void redepthChildren() {
|
|
|
|
_children.forEach(redepthChild);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void visitChildren(RenderObjectVisitor visitor) {
|
|
|
|
_children.forEach(visitor);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
List<DiagnosticsNode> debugDescribeChildren() {
|
|
|
|
final value = <DiagnosticsNode>[];
|
|
|
|
void add(RenderBox? child, String name) {
|
|
|
|
if (child != null) {
|
|
|
|
value.add(child.toDiagnosticsNode(name: name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
add(_leading, 'leading');
|
|
|
|
add(_body, 'body');
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool get sizedByParent => false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
double computeMinIntrinsicWidth(double height) {
|
|
|
|
_resolvePadding();
|
|
|
|
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
|
|
|
|
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
|
|
|
|
final leadingWidth = _leading == null
|
|
|
|
? 0
|
|
|
|
: _leading!.getMinIntrinsicWidth(height - verticalPadding) as int;
|
|
|
|
final bodyWidth = _body == null
|
|
|
|
? 0
|
|
|
|
: _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding))
|
|
|
|
as int;
|
|
|
|
return horizontalPadding + leadingWidth + bodyWidth;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
double computeMaxIntrinsicWidth(double height) {
|
|
|
|
_resolvePadding();
|
|
|
|
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
|
|
|
|
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
|
|
|
|
final leadingWidth = _leading == null
|
|
|
|
? 0
|
|
|
|
: _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int;
|
|
|
|
final bodyWidth = _body == null
|
|
|
|
? 0
|
|
|
|
: _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding))
|
|
|
|
as int;
|
|
|
|
return horizontalPadding + leadingWidth + bodyWidth;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
double computeMinIntrinsicHeight(double width) {
|
|
|
|
_resolvePadding();
|
|
|
|
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
|
|
|
|
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
|
|
|
|
if (_body != null) {
|
|
|
|
return _body!
|
|
|
|
.getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) +
|
|
|
|
verticalPadding;
|
|
|
|
}
|
|
|
|
return verticalPadding;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
double computeMaxIntrinsicHeight(double width) {
|
|
|
|
_resolvePadding();
|
|
|
|
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
|
|
|
|
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
|
|
|
|
if (_body != null) {
|
|
|
|
return _body!
|
|
|
|
.getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) +
|
|
|
|
verticalPadding;
|
|
|
|
}
|
|
|
|
return verticalPadding;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
double computeDistanceToActualBaseline(TextBaseline baseline) {
|
|
|
|
_resolvePadding();
|
|
|
|
return _body!.getDistanceToActualBaseline(baseline)! +
|
|
|
|
_resolvedPadding!.top;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void performLayout() {
|
|
|
|
final constraints = this.constraints;
|
|
|
|
_selectedRects = null;
|
|
|
|
|
|
|
|
_resolvePadding();
|
|
|
|
assert(_resolvedPadding != null);
|
|
|
|
|
|
|
|
if (_body == null && _leading == null) {
|
|
|
|
size = constraints.constrain(Size(
|
|
|
|
_resolvedPadding!.left + _resolvedPadding!.right,
|
|
|
|
_resolvedPadding!.top + _resolvedPadding!.bottom,
|
|
|
|
));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final innerConstraints = constraints.deflate(_resolvedPadding!);
|
|
|
|
|
|
|
|
final indentWidth = textDirection == TextDirection.ltr
|
|
|
|
? _resolvedPadding!.left
|
|
|
|
: _resolvedPadding!.right;
|
|
|
|
|
|
|
|
_body!.layout(innerConstraints, parentUsesSize: true);
|
|
|
|
(_body!.parentData as BoxParentData).offset =
|
|
|
|
Offset(_resolvedPadding!.left, _resolvedPadding!.top);
|
|
|
|
|
|
|
|
if (_leading != null) {
|
|
|
|
final leadingConstraints = innerConstraints.copyWith(
|
|
|
|
minWidth: indentWidth,
|
|
|
|
maxWidth: indentWidth,
|
|
|
|
maxHeight: _body!.size.height);
|
|
|
|
_leading!.layout(leadingConstraints, parentUsesSize: true);
|
|
|
|
(_leading!.parentData as BoxParentData).offset =
|
|
|
|
Offset(0, _resolvedPadding!.top);
|
|
|
|
}
|
|
|
|
|
|
|
|
size = constraints.constrain(Size(
|
|
|
|
_resolvedPadding!.left + _body!.size.width + _resolvedPadding!.right,
|
|
|
|
_resolvedPadding!.top + _body!.size.height + _resolvedPadding!.bottom,
|
|
|
|
));
|
|
|
|
|
|
|
|
_computeCaretPrototype();
|
|
|
|
}
|
|
|
|
|
|
|
|
CursorPainter get _cursorPainter => CursorPainter(
|
|
|
|
_body,
|
|
|
|
cursorCont.style,
|
|
|
|
_caretPrototype,
|
|
|
|
cursorCont.cursorColor.value,
|
|
|
|
devicePixelRatio,
|
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
void paint(PaintingContext context, Offset offset) {
|
|
|
|
if (_leading != null) {
|
|
|
|
final parentData = _leading!.parentData as BoxParentData;
|
|
|
|
final effectiveOffset = offset + parentData.offset;
|
|
|
|
context.paintChild(_leading!, effectiveOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_body != null) {
|
|
|
|
final parentData = _body!.parentData as BoxParentData;
|
|
|
|
final effectiveOffset = offset + parentData.offset;
|
|
|
|
if (enableInteractiveSelection &&
|
|
|
|
line.documentOffset <= textSelection.end &&
|
|
|
|
textSelection.start <= line.documentOffset + line.length - 1) {
|
|
|
|
final local = localSelection(line, textSelection, false);
|
|
|
|
_selectedRects ??= _body!.getBoxesForSelection(
|
|
|
|
local,
|
|
|
|
);
|
|
|
|
_paintSelection(context, effectiveOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasFocus &&
|
|
|
|
cursorCont.show.value &&
|
|
|
|
containsCursor() &&
|
|
|
|
!cursorCont.style.paintAboveText) {
|
|
|
|
_paintCursor(context, effectiveOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
context.paintChild(_body!, effectiveOffset);
|
|
|
|
|
|
|
|
if (hasFocus &&
|
|
|
|
cursorCont.show.value &&
|
|
|
|
containsCursor() &&
|
|
|
|
cursorCont.style.paintAboveText) {
|
|
|
|
_paintCursor(context, effectiveOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _paintSelection(PaintingContext context, Offset effectiveOffset) {
|
|
|
|
assert(_selectedRects != null);
|
|
|
|
final paint = Paint()..color = color;
|
|
|
|
for (final box in _selectedRects!) {
|
|
|
|
context.canvas.drawRect(box.toRect().shift(effectiveOffset), paint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _paintCursor(PaintingContext context, Offset effectiveOffset) {
|
|
|
|
final position = TextPosition(
|
|
|
|
offset: textSelection.extentOffset - line.documentOffset,
|
|
|
|
affinity: textSelection.base.affinity,
|
|
|
|
);
|
|
|
|
_cursorPainter.paint(context.canvas, effectiveOffset, position);
|
|
|
|
}
|
Add hitTesting to allow changing click behavior in embeds
The code change allows to override the default behaviour of clicking on
images and thus solves #90.
Now the only thing left to do is to use a `RawGestureDetector` in the
`EmbedBuilder` to deactivate the "old" behaviour (by entering the
GestureArena and win). The `embedBuilder` could look like this:
```dart
Widget embedBuilder(BuildContext context, Embed node, bool readOnly) {
return FutureBuilder<File>(
future: File('image_path.png'),
builder: (context, snapshot) {
final gestures = {
ReadDependentGestureRecognizer: GestureRecognizerFactoryWithHandlers<
ReadDependentGestureRecognizer>(
() => ReadDependentGestureRecognizer(readOnly: readOnly),
(instance) => instance
..onTapUp = ((_) => print('Test'))
)
};
return RawGestureDetector(
gestures: gestures,
child: Image.file(snapshot.data),
);
}
);
}
class ReadDependentGestureRecognizer extends TapGestureRecognizer {
ReadDependentGestureRecognizer({@required this.readOnly});
final bool readOnly;
@override
bool isPointerAllowed(PointerDownEvent event) {
if (!readOnly) {
return false;
}
return super.isPointerAllowed(event);
}
}
```
**Explanation of the code:**
When a `PointerDownEvent` is received by the `GestureBinding` a hit
test is performed to determine which `HitTestTarget` nodes are affected.
For this the (I think) element tree is traversed downwards to find
out which elements has been hit. To find out which elements were
hit, the method `hitTestSelf` and `hitTestChildren` are used. Since
the `hitTestChildren` method of `TextLine` has not been implemented
yet, `false` was always returned, which is why the hit test
never arrived at the `embedBuilder`. With the code change, the hit is
passed on and can be processed correctly in the embed.
Additionally I removed the class `_TransparentTapGestureRecognizer`,
because this class always accepted the gesture, even if the recognizer
lost in the arena.
4 years ago
|
|
|
|
|
|
|
@override
|
|
|
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
|
|
|
return _children.first.hitTest(result, position: position);
|
Add hitTesting to allow changing click behavior in embeds
The code change allows to override the default behaviour of clicking on
images and thus solves #90.
Now the only thing left to do is to use a `RawGestureDetector` in the
`EmbedBuilder` to deactivate the "old" behaviour (by entering the
GestureArena and win). The `embedBuilder` could look like this:
```dart
Widget embedBuilder(BuildContext context, Embed node, bool readOnly) {
return FutureBuilder<File>(
future: File('image_path.png'),
builder: (context, snapshot) {
final gestures = {
ReadDependentGestureRecognizer: GestureRecognizerFactoryWithHandlers<
ReadDependentGestureRecognizer>(
() => ReadDependentGestureRecognizer(readOnly: readOnly),
(instance) => instance
..onTapUp = ((_) => print('Test'))
)
};
return RawGestureDetector(
gestures: gestures,
child: Image.file(snapshot.data),
);
}
);
}
class ReadDependentGestureRecognizer extends TapGestureRecognizer {
ReadDependentGestureRecognizer({@required this.readOnly});
final bool readOnly;
@override
bool isPointerAllowed(PointerDownEvent event) {
if (!readOnly) {
return false;
}
return super.isPointerAllowed(event);
}
}
```
**Explanation of the code:**
When a `PointerDownEvent` is received by the `GestureBinding` a hit
test is performed to determine which `HitTestTarget` nodes are affected.
For this the (I think) element tree is traversed downwards to find
out which elements has been hit. To find out which elements were
hit, the method `hitTestSelf` and `hitTestChildren` are used. Since
the `hitTestChildren` method of `TextLine` has not been implemented
yet, `false` was always returned, which is why the hit test
never arrived at the `embedBuilder`. With the code change, the hit is
passed on and can be processed correctly in the embed.
Additionally I removed the class `_TransparentTapGestureRecognizer`,
because this class always accepted the gesture, even if the recognizer
lost in the arena.
4 years ago
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
void visitChildren(ElementVisitor visitor) {
|
|
|
|
_slotToChildren.values.forEach(visitor);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void 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
|
|
|
|
void mount(Element? parent, dynamic newSlot) {
|
|
|
|
super.mount(parent, newSlot);
|
|
|
|
_mountChild(widget.leading, TextLineSlot.LEADING);
|
|
|
|
_mountChild(widget.body, TextLineSlot.BODY);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void update(EditableTextLine newWidget) {
|
|
|
|
super.update(newWidget);
|
|
|
|
assert(widget == newWidget);
|
|
|
|
_updateChild(widget.leading, TextLineSlot.LEADING);
|
|
|
|
_updateChild(widget.body, TextLineSlot.BODY);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void insertRenderObjectChild(RenderBox child, TextLineSlot? slot) {
|
|
|
|
// assert(child is RenderBox);
|
|
|
|
_updateRenderObject(child, slot);
|
|
|
|
assert(renderObject.children.keys.contains(slot));
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void removeRenderObjectChild(RenderObject child, TextLineSlot? slot) {
|
|
|
|
assert(child is RenderBox);
|
|
|
|
assert(renderObject.children[slot!] == child);
|
|
|
|
_updateRenderObject(null, slot);
|
|
|
|
assert(!renderObject.children.keys.contains(slot));
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void moveRenderObjectChild(
|
|
|
|
RenderObject child, dynamic oldSlot, dynamic newSlot) {
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _mountChild(Widget? widget, TextLineSlot slot) {
|
|
|
|
final oldChild = _slotToChildren[slot];
|
|
|
|
final newChild = updateChild(oldChild, widget, slot);
|
|
|
|
if (oldChild != null) {
|
|
|
|
_slotToChildren.remove(slot);
|
|
|
|
}
|
|
|
|
if (newChild != null) {
|
|
|
|
_slotToChildren[slot] = newChild;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _updateRenderObject(RenderBox? child, TextLineSlot? slot) {
|
|
|
|
switch (slot) {
|
|
|
|
case TextLineSlot.LEADING:
|
|
|
|
renderObject.setLeading(child);
|
|
|
|
break;
|
|
|
|
case TextLineSlot.BODY:
|
|
|
|
renderObject.setBody(child as RenderContentProxyBox?);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _updateChild(Widget? widget, TextLineSlot slot) {
|
|
|
|
final oldChild = _slotToChildren[slot];
|
|
|
|
final newChild = updateChild(oldChild, widget, slot);
|
|
|
|
if (oldChild != null) {
|
|
|
|
_slotToChildren.remove(slot);
|
|
|
|
}
|
|
|
|
if (newChild != null) {
|
|
|
|
_slotToChildren[slot] = newChild;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|