import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_quill/models/documents/nodes/block.dart'; import 'package:flutter_quill/models/documents/nodes/node.dart'; import 'package:flutter_quill/widgets/text_selection.dart'; import 'package:tuple/tuple.dart'; import 'box.dart'; import 'delegate.dart'; import 'editor.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; final EmbedBuilder embedBuilder; EditableTextBlock( this.block, this.textDirection, this.verticalSpacing, this.textSelection, this.color, this.enableInteractiveSelection, this.hasFocus, this.contentPadding, this.embedBuilder) : assert(hasFocus != null), assert(embedBuilder != null); @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); throw UnimplementedError(); } } class RenderEditableTextBlock extends RenderEditableContainerBox implements RenderEditableBox { RenderEditableTextBlock({ List children, @required Block block, @required TextDirection textDirection, @required EdgeInsetsGeometry padding, @required Decoration decoration, ImageConfiguration configuration = ImageConfiguration.empty, EdgeInsets contentPadding = EdgeInsets.zero, }) : assert(block != null), assert(textDirection != null), assert(decoration != null), assert(padding != null), assert(contentPadding != null), _decoration = decoration, _configuration = configuration, _savedPadding = padding, _contentPadding = contentPadding, super( children, block, textDirection, padding.add(contentPadding), ); EdgeInsetsGeometry _savedPadding; EdgeInsets _contentPadding; set contentPadding(EdgeInsets value) { assert(value != null); if (_contentPadding == value) return; _contentPadding = value; super.setPadding(_savedPadding.add(_contentPadding)); } @override setPadding(EdgeInsetsGeometry value) { super.setPadding(value.add(_contentPadding)); _savedPadding = value; } BoxPainter _painter; Decoration get decoration => _decoration; Decoration _decoration; set decoration(Decoration value) { assert(value != null); if (value == _decoration) return; _painter?.dispose(); _painter = null; _decoration = value; markNeedsPaint(); } ImageConfiguration get configuration => _configuration; ImageConfiguration _configuration; set configuration(ImageConfiguration value) { assert(value != null); if (value == _configuration) return; _configuration = value; markNeedsPaint(); } @override TextRange getLineBoundary(TextPosition position) { RenderEditableBox child = childAtPosition(position); TextRange rangeInChild = child.getLineBoundary(TextPosition( offset: position.offset - child.getContainer().getOffset(), affinity: position.affinity, )); return TextRange( start: rangeInChild.start + child.getContainer().getOffset(), end: rangeInChild.end + child.getContainer().getOffset(), ); } @override Offset getOffsetForCaret(TextPosition position) { RenderEditableBox child = childAtPosition(position); return child.getOffsetForCaret(TextPosition( offset: position.offset - child.getContainer().getOffset(), affinity: position.affinity, )) + (child.parentData as BoxParentData).offset; } @override TextPosition getPositionForOffset(Offset offset) { RenderEditableBox child = childAtOffset(offset); BoxParentData parentData = child.parentData; TextPosition localPosition = child.getPositionForOffset(offset - parentData.offset); return TextPosition( offset: localPosition.offset + child.getContainer().getOffset(), affinity: localPosition.affinity, ); } @override TextRange getWordBoundary(TextPosition position) { RenderEditableBox child = childAtPosition(position); int nodeOffset = child.getContainer().getOffset(); TextRange childWord = child .getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); return TextRange( start: childWord.start + nodeOffset, end: childWord.end + nodeOffset, ); } @override TextPosition getPositionAbove(TextPosition position) { assert(position.offset < getContainer().length); RenderEditableBox child = childAtPosition(position); TextPosition childLocalPosition = TextPosition( offset: position.offset - child.getContainer().getOffset()); TextPosition result = child.getPositionAbove(childLocalPosition); if (result != null) { return TextPosition( offset: result.offset + child.getContainer().getOffset()); } RenderEditableBox sibling = childBefore(child); if (sibling == null) { return null; } Offset caretOffset = child.getOffsetForCaret(childLocalPosition); TextPosition testPosition = TextPosition(offset: sibling.getContainer().length - 1); Offset testOffset = sibling.getOffsetForCaret(testPosition); Offset finalOffset = Offset(caretOffset.dx, testOffset.dy); return TextPosition( offset: sibling.getContainer().getOffset() + sibling.getPositionForOffset(finalOffset).offset); } @override TextPosition getPositionBelow(TextPosition position) { assert(position.offset < getContainer().length); RenderEditableBox child = childAtPosition(position); TextPosition childLocalPosition = TextPosition( offset: position.offset - child.getContainer().getOffset()); TextPosition result = child.getPositionBelow(childLocalPosition); if (result != null) { return TextPosition( offset: result.offset + child.getContainer().getOffset()); } RenderEditableBox sibling = childAfter(child); if (sibling == null) { return null; } Offset caretOffset = child.getOffsetForCaret(childLocalPosition); Offset testOffset = sibling.getOffsetForCaret(TextPosition(offset: 0)); Offset finalOffset = Offset(caretOffset.dx, testOffset.dy); return TextPosition( offset: sibling.getContainer().getOffset() + sibling.getPositionForOffset(finalOffset).offset); } @override double preferredLineHeight(TextPosition position) { RenderEditableBox child = childAtPosition(position); return child.preferredLineHeight(TextPosition( offset: position.offset - child.getContainer().getOffset())); } @override TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( Offset(0.0, preferredLineHeight(selection.extent)) + getOffsetForCaret(selection.extent), null); } Node baseNode = getContainer().queryChild(selection.start, false).node; var baseChild = firstChild; while (baseChild != null) { if (baseChild.getContainer() == baseNode) { break; } baseChild = childAfter(baseChild); } assert(baseChild != null); TextSelectionPoint basePoint = baseChild.getBaseEndpointForSelection( localSelection(baseChild.getContainer(), selection, true)); return TextSelectionPoint( basePoint.point + (baseChild.parentData as BoxParentData).offset, basePoint.direction); } @override TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( Offset(0.0, preferredLineHeight(selection.extent)) + getOffsetForCaret(selection.extent), null); } Node extentNode = getContainer().queryChild(selection.end, false).node; var extentChild = firstChild; while (extentChild != null) { if (extentChild.getContainer() == extentNode) { break; } extentChild = childAfter(extentChild); } assert(extentChild != null); TextSelectionPoint extentPoint = extentChild.getExtentEndpointForSelection( localSelection(extentChild.getContainer(), selection, true)); return TextSelectionPoint( extentPoint.point + (extentChild.parentData as BoxParentData).offset, extentPoint.direction); } @override void detach() { _painter?.dispose(); _painter = null; super.detach(); markNeedsPaint(); } @override paint(PaintingContext context, Offset offset) { _paintDecoration(context, offset); defaultPaint(context, offset); } _paintDecoration(PaintingContext context, Offset offset) { assert(size.width != null); assert(size.height != null); _painter ??= _decoration.createBoxPainter(markNeedsPaint); EdgeInsets decorationPadding = resolvedPadding - _contentPadding; ImageConfiguration filledConfiguration = configuration.copyWith(size: decorationPadding.deflateSize(size)); int debugSaveCount = context.canvas.getSaveCount(); final decorationOffset = offset.translate(decorationPadding.left, decorationPadding.top); _painter.paint(context.canvas, decorationOffset, filledConfiguration); if (debugSaveCount != context.canvas.getSaveCount()) { throw ('${_decoration.runtimeType} painter had mismatching save and restore calls.'); } if (decoration.isComplex) { context.setIsComplexHint(); } } @override bool hitTestChildren(BoxHitTestResult result, {Offset position}) { return defaultHitTestChildren(result, position: position); } } class _EditableBlock extends MultiChildRenderObjectWidget { final Block block; final TextDirection textDirection; final Tuple2 padding; final Decoration decoration; final EdgeInsets contentPadding; _EditableBlock(this.block, this.textDirection, this.padding, this.decoration, this.contentPadding, List children) : assert(block != null), assert(textDirection != null), assert(padding != null), assert(decoration != null), assert(children != null), super(children: children); EdgeInsets get _padding => EdgeInsets.only(top: padding.item1, bottom: padding.item2); EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero; @override RenderEditableTextBlock createRenderObject(BuildContext context) { return RenderEditableTextBlock( block: block, textDirection: textDirection, padding: _padding, decoration: decoration, contentPadding: _contentPadding, ); } @override void updateRenderObject( BuildContext context, covariant RenderEditableTextBlock renderObject) { renderObject.setContainer(block); renderObject.setTextDirection(textDirection); renderObject.setPadding(_padding); renderObject.decoration = decoration; renderObject.contentPadding = _contentPadding; } } class _NumberPoint extends StatelessWidget { final int index; final int count; final TextStyle style; final double width; final bool withDot; final double padding; const _NumberPoint({ Key key, @required this.index, @required this.count, @required this.style, @required this.width, this.withDot = true, this.padding = 0.0, }) : super(key: key); @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, child: Text(withDot ? '$index.' : '$index', style: style), width: width, padding: EdgeInsetsDirectional.only(end: padding), ); } } class _BulletPoint extends StatelessWidget { final TextStyle style; final double width; const _BulletPoint({ Key key, @required this.style, @required this.width, }) : super(key: key); @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, child: Text('•', style: style), width: width, padding: EdgeInsetsDirectional.only(end: 13.0), ); } }