From 23d26592c2da9b31101e6e74b2e5f3e72e8eadce Mon Sep 17 00:00:00 2001 From: singerdmx Date: Sat, 19 Dec 2020 12:00:14 -0800 Subject: [PATCH] Implement RawEditorState class --- lib/widgets/editor.dart | 366 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 331 insertions(+), 35 deletions(-) diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 5ee35a19..9a078f4f 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -462,6 +462,12 @@ class RawEditorState extends EditorState bool get _hasFocus => widget.focusNode.hasFocus; + TextDirection get _textDirection { + TextDirection result = Directionality.of(context); + assert(result != null); + return result; + } + handleCursorMovement( LogicalKeyboardKey key, bool wordModifier, @@ -811,9 +817,7 @@ class RawEditorState extends EditorState } @override - void bringIntoView(TextPosition position) { - // TODO: implement bringIntoView - } + void bringIntoView(TextPosition position) {} @override void connectionClosed() { @@ -832,10 +836,36 @@ class RawEditorState extends EditorState _focusAttachment.reparent(); super.build(context); - // TODO - throw UnimplementedError(); + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: _Editor( + key: _editorKey, + children: _buildChildren(context), + document: widget.controller.document, + selection: widget.controller.selection, + hasFocus: _hasFocus, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + padding: widget.padding, + ), + ), + ); } + _handleSelectionChanged( + TextSelection selection, SelectionChangedCause cause) { + widget.controller.updateSelection(selection, ChangeSource.LOCAL); + + _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); + + requestKeyboard(); + } + + _buildChildren(BuildContext context) {} + @override void initState() { super.initState(); @@ -1227,7 +1257,6 @@ class RenderEditor extends RenderEditableContainerBox RenderEditor( List children, TextDirection textDirection, - hasFocus, EdgeInsetsGeometry padding, this.document, this.selection, @@ -1238,7 +1267,7 @@ class RenderEditor extends RenderEditableContainerBox EdgeInsets floatingCursorAddedMargin) : assert(document != null), assert(textDirection != null), - assert(hasFocus != null), + assert(_hasFocus != null), assert(floatingCursorAddedMargin != null), super( children, @@ -1292,66 +1321,280 @@ class RenderEditor extends RenderEditableContainerBox @override List getEndpointsForSelection( TextSelection textSelection) { - // TODO: implement getEndpointsForSelection - throw UnimplementedError(); + assert(constraints != null); + + if (selection.isCollapsed) { + RenderEditableBox child = childAtPosition(selection.extent); + TextPosition localPosition = TextPosition( + offset: selection.extentOffset - child.getContainer().getOffset()); + Offset localOffset = child.getOffsetForCaret(localPosition); + BoxParentData parentData = child.parentData; + return [ + TextSelectionPoint( + Offset(0.0, child.preferredLineHeight(localPosition)) + + localOffset + + parentData.offset, + null) + ]; + } + + Node baseNode = _container.queryChild(selection.start, false).node; + + var baseChild = firstChild; + while (baseChild != null) { + if (baseChild.getContainer() == baseNode) { + break; + } + baseChild = childAfter(baseChild); + } + assert(baseChild != null); + + BoxParentData baseParentData = baseChild.parentData; + TextSelection baseSelection = + localSelection(baseChild.getContainer(), selection, true); + TextSelectionPoint basePoint = + baseChild.getBaseEndpointForSelection(baseSelection); + basePoint = TextSelectionPoint( + basePoint.point + baseParentData.offset, basePoint.direction); + + Node extentNode = _container.queryChild(selection.end, false).node; + var extentChild = baseChild; + while (extentChild != null) { + if (extentChild.getContainer() == extentNode) { + break; + } + extentChild = childAfter(extentChild); + } + assert(extentChild != null); + + BoxParentData extentParentData = extentChild.parentData; + TextSelection extentSelection = + localSelection(extentChild.getContainer(), selection, true); + TextSelectionPoint extentPoint = + extentChild.getExtentEndpointForSelection(extentSelection); + extentPoint = TextSelectionPoint( + extentPoint.point + extentParentData.offset, extentPoint.direction); + + return [basePoint, extentPoint]; } + Offset _lastTapDownPosition; + @override - TextPosition getPositionForOffset(Offset offset) { - // TODO: implement getPositionForOffset - throw UnimplementedError(); + handleTapDown(TapDownDetails details) { + _lastTapDownPosition = details.globalPosition; } @override - void handleTapDown(TapDownDetails details) { - // TODO: implement handleTapDown + selectWordsInRange( + Offset from, + Offset to, + SelectionChangedCause cause, + ) { + assert(cause != null); + assert(from != null); + if (onSelectionChanged == null) { + return; + } + TextPosition firstPosition = getPositionForOffset(from); + TextSelection firstWord = selectWordAtPosition(firstPosition); + TextSelection lastWord = + to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to)); + + _handleSelectionChange( + TextSelection( + baseOffset: firstWord.base.offset, + extentOffset: lastWord.extent.offset, + affinity: firstWord.affinity, + ), + cause, + ); } - @override - double preferredLineHeight(TextPosition position) { - // TODO: implement preferredLineHeight - throw UnimplementedError(); + _handleSelectionChange( + TextSelection nextSelection, + SelectionChangedCause cause, + ) { + bool focusingEmpty = nextSelection.baseOffset == 0 && + nextSelection.extentOffset == 0 && + !_hasFocus; + if (nextSelection == selection && + cause != SelectionChangedCause.keyboard && + !focusingEmpty) { + return; + } + if (onSelectionChanged != null) { + onSelectionChanged(nextSelection, cause); + } } @override - TextSelection selectLineAtPosition(TextPosition position) { - // TODO: implement selectLineAtPosition - throw UnimplementedError(); + selectWordEdge(SelectionChangedCause cause) { + assert(cause != null); + assert(_lastTapDownPosition != null); + if (onSelectionChanged == null) { + return; + } + TextPosition position = getPositionForOffset(_lastTapDownPosition); + RenderEditableBox child = childAtPosition(position); + int nodeOffset = child.getContainer().getOffset(); + TextPosition localPosition = TextPosition( + offset: position.offset - nodeOffset, + affinity: position.affinity, + ); + TextRange localWord = child.getWordBoundary(localPosition); + TextRange word = TextRange( + start: localWord.start + nodeOffset, + end: localWord.end + nodeOffset, + ); + if (position.offset - word.start <= 1) { + _handleSelectionChange( + TextSelection.collapsed( + offset: word.start, affinity: TextAffinity.downstream), + cause, + ); + } else { + _handleSelectionChange( + TextSelection.collapsed( + offset: word.end, affinity: TextAffinity.upstream), + cause, + ); + } } @override - void selectPosition(SelectionChangedCause cause) { - // TODO: implement selectPosition + selectPositionAt( + Offset from, + Offset to, + SelectionChangedCause cause, + ) { + assert(cause != null); + assert(from != null); + if (onSelectionChanged == null) { + return; + } + TextPosition fromPosition = getPositionForOffset(from); + TextPosition toPosition = to == null ? null : getPositionForOffset(to); + + int baseOffset = fromPosition.offset; + int extentOffset = fromPosition.offset; + if (toPosition != null) { + baseOffset = math.min(fromPosition.offset, toPosition.offset); + extentOffset = math.max(fromPosition.offset, toPosition.offset); + } + + TextSelection newSelection = TextSelection( + baseOffset: baseOffset, + extentOffset: extentOffset, + affinity: fromPosition.affinity, + ); + _handleSelectionChange(newSelection, cause); } @override - void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause) { - // TODO: implement selectPositionAt + selectWord(SelectionChangedCause cause) { + selectWordsInRange(_lastTapDownPosition, null, cause); } @override - void selectWord(SelectionChangedCause cause) { - // TODO: implement selectWord + selectPosition(SelectionChangedCause cause) { + selectPositionAt(_lastTapDownPosition, null, cause); } @override TextSelection selectWordAtPosition(TextPosition position) { - // TODO: implement selectWordAtPosition - throw UnimplementedError(); + RenderEditableBox child = childAtPosition(position); + int nodeOffset = child.getContainer().getOffset(); + TextPosition localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + TextRange localWord = child.getWordBoundary(localPosition); + TextRange word = TextRange( + start: localWord.start + nodeOffset, + end: localWord.end + nodeOffset, + ); + if (position.offset >= word.end) { + return TextSelection.fromPosition(position); + } + return TextSelection(baseOffset: word.start, extentOffset: word.end); + } + + @override + TextSelection selectLineAtPosition(TextPosition position) { + RenderEditableBox child = childAtPosition(position); + int nodeOffset = child.getContainer().getOffset(); + TextPosition localPosition = TextPosition( + offset: position.offset - nodeOffset, affinity: position.affinity); + TextRange localLineRange = child.getLineBoundary(localPosition); + TextRange line = TextRange( + start: localLineRange.start + nodeOffset, + end: localLineRange.end + nodeOffset, + ); + + if (position.offset >= line.end) { + return TextSelection.fromPosition(position); + } + return TextSelection(baseOffset: line.start, extentOffset: line.end); } @override - void selectWordEdge(SelectionChangedCause cause) { - // TODO: implement selectWordEdge + void paint(PaintingContext context, Offset offset) { + defaultPaint(context, offset); + _paintHandleLayers(context, getEndpointsForSelection(selection)); } @override - void selectWordsInRange(Offset from, Offset to, SelectionChangedCause cause) { - assert(cause != null && from != null); - // TODO: implement selectWordsInRange + bool hitTestChildren(BoxHitTestResult result, {Offset position}) { + return defaultHitTestChildren(result, position: position); } - getOffsetToRevealCursor( + _paintHandleLayers( + PaintingContext context, List endpoints) { + var startPoint = endpoints[0].point; + startPoint = Offset( + startPoint.dx.clamp(0.0, size.width), + startPoint.dy.clamp(0.0, size.height), + ); + context.pushLayer( + LeaderLayer(link: _startHandleLayerLink, offset: startPoint), + super.paint, + Offset.zero, + ); + if (endpoints.length == 2) { + var endPoint = endpoints[1].point; + endPoint = Offset( + endPoint.dx.clamp(0.0, size.width), + endPoint.dy.clamp(0.0, size.height), + ); + context.pushLayer( + LeaderLayer(link: _endHandleLayerLink, offset: endPoint), + super.paint, + Offset.zero, + ); + } + } + + @override + double preferredLineHeight(TextPosition position) { + RenderEditableBox child = childAtPosition(position); + return child.preferredLineHeight(TextPosition( + offset: position.offset - child.getContainer().getOffset())); + } + + @override + TextPosition getPositionForOffset(Offset offset) { + Offset local = globalToLocal(offset); + RenderEditableBox child = childAtOffset(local); + + BoxParentData parentData = child.parentData; + Offset localOffset = local - parentData.offset; + TextPosition localPosition = child.getPositionForOffset(localOffset); + return TextPosition( + offset: localPosition.offset + child.getContainer().getOffset(), + affinity: localPosition.affinity, + ); + } + + double getOffsetToRevealCursor( double viewportHeight, double scrollOffset, double offsetInViewport) { List endpoints = getEndpointsForSelection(selection); if (endpoints.length != 1) { @@ -1596,3 +1839,56 @@ class RenderEditableContainerBox extends RenderBox _resolvedPadding.top; } } + +class _Editor extends MultiChildRenderObjectWidget { + _Editor({ + @required Key key, + @required List children, + @required this.document, + @required this.textDirection, + @required this.hasFocus, + @required this.selection, + @required this.startHandleLayerLink, + @required this.endHandleLayerLink, + @required this.onSelectionChanged, + this.padding = EdgeInsets.zero, + }) : super(key: key, children: children); + + final Document document; + final TextDirection textDirection; + final bool hasFocus; + final TextSelection selection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final EdgeInsetsGeometry padding; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + null, + textDirection, + padding, + document, + selection, + hasFocus, + onSelectionChanged, + startHandleLayerLink, + endHandleLayerLink, + EdgeInsets.fromLTRB(4, 4, 4, 5)); + } + + @override + updateRenderObject( + BuildContext context, covariant RenderEditor renderObject) { + renderObject.document = document; + renderObject.setContainer(document.root); + renderObject.textDirection = textDirection; + renderObject.setHasFocus(hasFocus); + renderObject.setSelection(selection); + renderObject.setStartHandleLayerLink(startHandleLayerLink); + renderObject.setEndHandleLayerLink(endHandleLayerLink); + renderObject.onSelectionChanged = onSelectionChanged; + renderObject.setPadding(padding); + } +}