import 'package:flutter/foundation.dart'; 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/block.dart'; import 'package:flutter_quill/models/documents/nodes/line.dart'; import 'package:flutter_quill/models/documents/nodes/node.dart'; import 'package:flutter_quill/widgets/cursor.dart'; import 'package:flutter_quill/widgets/default_styles.dart'; import 'package:flutter_quill/widgets/text_line.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; final CursorCont cursorCont; EditableTextBlock( this.block, this.textDirection, this.verticalSpacing, this.textSelection, this.color, this.enableInteractiveSelection, this.hasFocus, this.contentPadding, this.embedBuilder, this.cursorCont) : assert(hasFocus != null), assert(embedBuilder != null), assert(cursorCont != null); @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); DefaultStyles defaultStyles = QuillStyles.getStyles(context, false); return _EditableBlock( block, textDirection, verticalSpacing, _getDecorationForBlock(block, defaultStyles) ?? BoxDecoration(), contentPadding, _buildChildren(context)); } BoxDecoration _getDecorationForBlock( Block node, DefaultStyles defaultStyles) { Map attrs = block.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return defaultStyles.quote.decoration; } if (attrs.containsKey(Attribute.codeBlock.key)) { return defaultStyles.code.decoration; } return null; } List _buildChildren(BuildContext context) { DefaultStyles defaultStyles = QuillStyles.getStyles(context, false); int count = block.children.length; var children = []; int index = 0; for (Line line in block.children) { index++; children.add(EditableTextLine( line, _buildLeading(context, line, index, count), TextLine( line: line, textDirection: textDirection, embedBuilder: embedBuilder, ), _getIndentWidth(), _getSpacingForLine(line, index, count, defaultStyles), textDirection, textSelection, color, enableInteractiveSelection, hasFocus, MediaQuery.of(context).devicePixelRatio, cursorCont)); } return children.toList(growable: false); } Widget _buildLeading(BuildContext context, Line node, int index, int count) { DefaultStyles defaultStyles = QuillStyles.getStyles(context, false); Map attrs = block.style.attributes; if (attrs[Attribute.list.key] == Attribute.ol) { return _NumberPoint( index: index, count: count, style: defaultStyles.paragraph.style, width: 32.0, padding: 8.0, ); } if (attrs[Attribute.list.key] == Attribute.ul) { return _BulletPoint( style: defaultStyles.paragraph.style.copyWith(fontWeight: FontWeight.bold), width: 32, ); } if (attrs.containsKey(Attribute.codeBlock.key)) { return _NumberPoint( index: index, count: count, style: defaultStyles.code.style .copyWith(color: defaultStyles.code.style.color.withOpacity(0.4)), width: 32.0, padding: 16.0, withDot: false, ); } return null; } double _getIndentWidth() { Map attrs = block.style.attributes; if (attrs.containsKey(Attribute.blockQuote.key)) { return 16.0; } return 32.0; } Tuple2 _getSpacingForLine( Line node, int index, int count, DefaultStyles defaultStyles) { double top = 0.0, bottom = 0.0; Map attrs = block.style.attributes; if (attrs.containsKey(Attribute.header.key)) { int level = attrs[Attribute.header.key].value; switch (level) { case 1: top = defaultStyles.h1.verticalSpacing.item1; bottom = defaultStyles.h1.verticalSpacing.item2; break; case 2: top = defaultStyles.h2.verticalSpacing.item1; bottom = defaultStyles.h2.verticalSpacing.item2; break; case 3: top = defaultStyles.h3.verticalSpacing.item1; bottom = defaultStyles.h3.verticalSpacing.item2; break; default: throw ('Invalid level $level'); } } else { Tuple2 lineSpacing; if (attrs.containsKey(Attribute.blockQuote.key)) { lineSpacing = defaultStyles.quote.lineSpacing; } else if (attrs.containsKey(Attribute.list.key)) { lineSpacing = defaultStyles.lists.lineSpacing; } else if (attrs.containsKey(Attribute.codeBlock.key)) { lineSpacing = defaultStyles.code.lineSpacing; } } if (index == 1) { top = 0.0; } if (index == count) { bottom = 0.0; } return Tuple2(top, bottom); } } 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), ); } }