diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index a983ad8e..b13f2115 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -6,7 +6,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/models/documents/document.dart'; import 'package:flutter_quill/models/documents/nodes/container.dart' - as container; + as containerNode; import 'package:flutter_quill/models/documents/nodes/node.dart'; import 'package:flutter_quill/utils/diff_delta.dart'; import 'package:flutter_quill/widgets/default_styles.dart'; @@ -1010,7 +1010,7 @@ class RenderEditableContainerBox extends RenderBox EditableContainerParentData>, RenderBoxContainerDefaultsMixin { - container.Container _container; + containerNode.Container _container; TextDirection _textDirection; EdgeInsetsGeometry _padding; EdgeInsets _resolvedPadding; @@ -1024,7 +1024,11 @@ class RenderEditableContainerBox extends RenderBox addAll(children); } - setContainer(container.Container c) { + containerNode.Container getContainer() { + return _container; + } + + setContainer(containerNode.Container c) { assert(c != null); if (_container == c) { return; @@ -1052,6 +1056,8 @@ class RenderEditableContainerBox extends RenderBox _markNeedsPaddingResolution(); } + EdgeInsets get resolvedPadding => _resolvedPadding; + _resolvePadding() { if (_resolvedPadding != null) { return; diff --git a/lib/widgets/text_block.dart b/lib/widgets/text_block.dart index 24e2a5e1..2de4cb99 100644 --- a/lib/widgets/text_block.dart +++ b/lib/widgets/text_block.dart @@ -1,8 +1,14 @@ +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; @@ -30,12 +36,370 @@ class EditableTextBlock extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: implement build + assert(debugCheckHasMediaQuery(context)); + throw UnimplementedError(); } } -//class RenderEditableTextBlock extends RenderEditableContainerBox -// implements RenderEditableBox { -// -// } +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), + ); + } +} diff --git a/lib/widgets/text_selection.dart b/lib/widgets/text_selection.dart index 4bb6cc96..e7fe34f6 100644 --- a/lib/widgets/text_selection.dart +++ b/lib/widgets/text_selection.dart @@ -1,9 +1,22 @@ +import 'dart:math' as math; + import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_quill/models/documents/nodes/node.dart'; import 'editor.dart'; +TextSelection localSelection(Node node, TextSelection selection, fromParent) { + int base = fromParent ? node.getOffset() : node.getDocumentOffset(); + assert(base <= selection.end && selection.start <= base + node.length - 1); + + int offset = fromParent ? node.getOffset() : node.getDocumentOffset(); + return selection.copyWith( + baseOffset: math.max(selection.start - offset, 0), + extentOffset: math.min(selection.end - offset, node.length - 1)); +} + class EditorTextSelectionOverlay { TextEditingValue value; bool handlesVisible = false; @@ -103,5 +116,4 @@ class EditorTextSelectionOverlay { hide(); _toolbarController.dispose(); } - }