| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- import 'dart:math' as math;
- import 'package:flowy_editor/widget/selection.dart';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import '../model/document/node/container.dart' as node;
- import '../model/document/document.dart';
- import 'box.dart';
- typedef TextSelectionChangeHandler = void Function(
- TextSelection selection,
- SelectionChangedCause cause,
- );
- /* ----------------------------- Abstract Editor ---------------------------- */
- abstract class RenderAbstractEditor {
- TextSelection selectWordAtPosition(TextPosition position);
- TextSelection selectLineAtPosition(TextPosition position);
- double preferredLineHeight(TextPosition position);
- TextPosition getPositionForOffset(Offset offset);
- List<TextSelectionPoint> getEndpointsForSelection(
- TextSelection textSelection);
- void handleTapDown(TapDownDetails details);
- void selectWordsInRange(Offset from, Offset to, SelectionChangedCause cause);
- void selectWordEdge(SelectionChangedCause cause);
- void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause);
- void selectWord(SelectionChangedCause cause);
- void selectPosition(SelectionChangedCause cause);
- }
- /* ------------------------------ Container Box ----------------------------- */
- class EditableContainerParentData
- extends ContainerBoxParentData<RenderEditableBox> {}
- class RenderEditableContainerBox extends RenderBox
- with
- ContainerRenderObjectMixin<RenderEditableBox,
- EditableContainerParentData>,
- RenderBoxContainerDefaultsMixin<RenderEditableBox,
- EditableContainerParentData> {
- RenderEditableContainerBox(
- List<RenderEditableBox>? children,
- this.textDirection,
- this.scrollBottomInset,
- this._container,
- this._padding,
- ) : assert(_padding.isNonNegative) {
- addAll(children);
- }
- TextDirection textDirection;
- double scrollBottomInset;
- node.Container _container;
- EdgeInsetsGeometry _padding;
- EdgeInsets? _resolvedPadding;
- node.Container get container => _container;
- set container(node.Container container) {
- if (_container == container) {
- return;
- }
- _container = container;
- markNeedsLayout();
- }
- EdgeInsetsGeometry get padding => _padding;
- set padding(EdgeInsetsGeometry value) {
- assert(value.isNonNegative);
- if (_padding == value) {
- return;
- }
- _padding = value;
- _markNeedsPaddingResolution();
- }
- EdgeInsets? get resolvedPadding => _resolvedPadding;
- void _resolvePadding() {
- if (_resolvedPadding != null) {
- return;
- }
- _resolvedPadding = _padding.resolve(textDirection);
- _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left);
- assert(_resolvedPadding!.isNonNegative);
- }
- void _markNeedsPaddingResolution() {
- _resolvedPadding = null;
- markNeedsLayout();
- }
- RenderEditableBox childAtPosition(TextPosition position) {
- assert(firstChild != null);
- final targetNode = _container.queryChild(position.offset, false).node;
- var targetChild = firstChild;
- while (targetChild != null) {
- if (targetChild.container == targetNode) {
- break;
- }
- targetChild = childAfter(targetChild);
- }
- if (targetChild == null) {
- throw '`targetChild` should not be null';
- }
- return targetChild;
- }
- RenderEditableBox? childAtOffset(Offset offset) {
- assert(firstChild != null);
- _resolvePadding();
- if (offset.dy <= _resolvedPadding!.top) {
- return firstChild;
- }
- if (offset.dy >= size.height - _resolvedPadding!.bottom) {
- return lastChild;
- }
- var child = firstChild;
- final dx = -offset.dx;
- var dy = _resolvedPadding!.top;
- while (child != null) {
- if (child.size.contains(offset.translate(dx, -dy))) {
- return child;
- }
- dy += child.size.height;
- child = childAfter(child);
- }
- throw 'No child';
- }
- @override
- void setupParentData(covariant RenderBox child) {
- if (child.parent is EditableContainerParentData) {
- return;
- }
- child.parentData = EditableContainerParentData();
- }
- @override
- void performLayout() {
- assert(constraints.hasBoundedWidth);
- assert(!constraints.hasBoundedHeight);
- _resolvePadding();
- assert(_resolvedPadding != null);
- var mainAxisExtent = _resolvedPadding!.top;
- var child = firstChild;
- final innerConstraints =
- BoxConstraints.tightFor(width: constraints.maxWidth)
- .deflate(_resolvedPadding!);
- while (child != null) {
- child.layout(innerConstraints, parentUsesSize: true);
- final childParentData = (child.parentData as EditableContainerParentData)
- ..offset = Offset(_resolvedPadding!.left, mainAxisExtent);
- mainAxisExtent += child.size.height;
- assert(child.parentData == childParentData);
- child = childParentData.nextSibling;
- }
- mainAxisExtent += _resolvedPadding!.bottom;
- size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
- assert(size.isFinite);
- }
- double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) {
- var extent = 0.0;
- var child = firstChild;
- while (child != null) {
- extent = math.max(extent, childSize(child));
- final childParentData = child.parentData as EditableContainerParentData;
- child = childParentData.nextSibling;
- }
- return extent;
- }
- double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) {
- var extent = 0.0;
- var child = firstChild;
- while (child != null) {
- extent += childSize(child);
- final childParentData = child.parentData as EditableContainerParentData;
- child = childParentData.nextSibling;
- }
- return extent;
- }
- @override
- double computeMinIntrinsicWidth(double height) {
- _resolvePadding();
- return _getIntrinsicCrossAxis((child) {
- final childHeight = math.max<double>(
- 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
- return child.getMinIntrinsicWidth(childHeight) +
- _resolvedPadding!.left +
- _resolvedPadding!.right;
- });
- }
- @override
- double computeMaxIntrinsicWidth(double height) {
- _resolvePadding();
- return _getIntrinsicCrossAxis((child) {
- final childHeight = math.max<double>(
- 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
- return child.getMaxIntrinsicWidth(childHeight) +
- _resolvedPadding!.left +
- _resolvedPadding!.right;
- });
- }
- @override
- double computeMinIntrinsicHeight(double width) {
- _resolvePadding();
- return _getIntrinsicMainAxis((child) {
- final childWidth = math.max<double>(
- 0, width - _resolvedPadding!.left + _resolvedPadding!.right);
- return child.getMinIntrinsicHeight(childWidth) +
- _resolvedPadding!.top +
- _resolvedPadding!.bottom;
- });
- }
- @override
- double computeMaxIntrinsicHeight(double width) {
- _resolvePadding();
- return _getIntrinsicMainAxis((child) {
- final childWidth = math.max<double>(
- 0, width - _resolvedPadding!.left + _resolvedPadding!.right);
- return child.getMaxIntrinsicHeight(childWidth) +
- _resolvedPadding!.top +
- _resolvedPadding!.bottom;
- });
- }
- @override
- double? computeDistanceToActualBaseline(TextBaseline baseline) {
- _resolvePadding();
- return defaultComputeDistanceToFirstActualBaseline(baseline)! +
- _resolvedPadding!.top;
- }
- }
- /* ------------------------------ Render Editor ----------------------------- */
- class RenderEditor extends RenderEditableContainerBox
- implements RenderAbstractEditor {
- RenderEditor(
- List<RenderEditableBox>? children,
- TextDirection textDirection,
- double scrollBottomInset,
- EdgeInsetsGeometry padding,
- EdgeInsets floatingCursorAddedMargin,
- this._document,
- this._selection,
- this._hasFocus,
- this.onSelectionChanged,
- this._startHandleLayerLink,
- this._endHandleLayerLink,
- ) : super(
- children,
- textDirection,
- scrollBottomInset,
- _document.root,
- padding,
- );
- TextSelectionChangeHandler onSelectionChanged;
- Document _document;
- TextSelection _selection;
- final ValueNotifier<bool> _selectionStartInViewport =
- ValueNotifier<bool>(true);
- final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);
- bool _hasFocus = false;
- LayerLink _startHandleLayerLink;
- LayerLink _endHandleLayerLink;
- Offset? _lastTapDownPosition;
- Document get document => _document;
- ValueListenable<bool> get selectionStartInViewport =>
- _selectionStartInViewport;
- ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
- set document(Document value) {
- if (_document == value) {
- return;
- }
- _document = value;
- markNeedsLayout();
- }
- set hasFocus(bool value) {
- if (_hasFocus == value) {
- return;
- }
- _hasFocus = value;
- markNeedsSemanticsUpdate();
- }
- set selection(TextSelection value) {
- if (_selection == value) {
- return;
- }
- _selection = value;
- markNeedsPaint();
- }
- set startHandleLayerLink(LayerLink value) {
- if (_startHandleLayerLink == value) {
- return;
- }
- _startHandleLayerLink = value;
- markNeedsPaint();
- }
- set endHandleLayerLink(LayerLink value) {
- if (_endHandleLayerLink == value) {
- return;
- }
- _endHandleLayerLink = value;
- markNeedsPaint();
- }
- @override
- set scrollBottomInset(double value) {
- if (scrollBottomInset == value) {
- return;
- }
- scrollBottomInset = value;
- markNeedsPaint();
- }
- @override
- List<TextSelectionPoint> getEndpointsForSelection(
- TextSelection textSelection) {
- if (textSelection.isCollapsed) {
- final child = childAtPosition(textSelection.extent);
- final localPosition = TextPosition(
- offset: textSelection.extentOffset - child.container.offset,
- );
- final localOffset = child.getOffsetForCaret(localPosition);
- final parentData = child.parentData as BoxParentData;
- return [
- TextSelectionPoint(
- Offset(0, child.preferredLineHeight(localPosition)) +
- localOffset +
- parentData.offset,
- null,
- )
- ];
- }
- final baseNode = _container.queryChild(textSelection.start, false).node;
- var baseChild = firstChild;
- while (baseChild != null) {
- if (baseChild.container == baseNode) {
- break;
- }
- baseChild = childAfter(baseChild);
- }
- assert(baseChild != null);
- final baseParentData = baseChild!.parentData as BoxParentData;
- final baseSelection =
- localSelection(baseChild.container, textSelection, true);
- var basePoint = baseChild.getBaseEndpointForSelection(baseSelection);
- basePoint = TextSelectionPoint(
- basePoint.point + baseParentData.offset,
- basePoint.direction,
- );
- final extentNode = _container.queryChild(textSelection.end, false).node;
- RenderEditableBox? extentChild = baseChild;
- while (extentChild != null) {
- if (extentChild.container == extentNode) {
- break;
- }
- extentChild = childAfter(extentChild);
- }
- assert(extentChild != null);
- final extentParentData = extentChild!.parentData as BoxParentData;
- final extentSelection =
- localSelection(extentChild.container, textSelection, true);
- var extentPoint =
- extentChild.getExtentEndpointForSelection(extentSelection);
- extentPoint = TextSelectionPoint(
- extentPoint.point + extentParentData.offset,
- extentPoint.direction,
- );
- return <TextSelectionPoint>[basePoint, extentPoint];
- }
- @override
- TextPosition getPositionForOffset(Offset offset) {
- final local = globalToLocal(offset);
- final child = childAtOffset(local)!;
- final parentData = child.parentData as BoxParentData;
- final localOffset = local - parentData.offset;
- final localPosition = child.getPositionForOffset(localOffset);
- return TextPosition(
- offset: localPosition.offset + child.container.offset,
- affinity: localPosition.affinity,
- );
- }
- @override
- void handleTapDown(TapDownDetails details) {
- _lastTapDownPosition = details.globalPosition;
- }
- @override
- double preferredLineHeight(TextPosition position) {
- final child = childAtPosition(position);
- return child.preferredLineHeight(
- TextPosition(offset: position.offset - child.container.offset),
- );
- }
- @override
- TextSelection selectLineAtPosition(TextPosition position) {
- final child = childAtPosition(position);
- final nodeOffset = child.container.offset;
- final localPosition = TextPosition(
- offset: position.offset - nodeOffset,
- affinity: position.affinity,
- );
- final localLineRange = child.getLineBoundary(localPosition);
- final 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 selectPosition(SelectionChangedCause cause) {
- selectPositionAt(_lastTapDownPosition!, null, cause);
- }
- @override
- void selectPositionAt(Offset from, Offset? to, SelectionChangedCause cause) {
- final fromPosition = getPositionForOffset(from);
- final toPosition = to == null ? null : getPositionForOffset(to);
- var baseOffset = fromPosition.offset;
- var extentOffset = fromPosition.offset;
- if (toPosition != null) {
- baseOffset = math.min(fromPosition.offset, toPosition.offset);
- extentOffset = math.max(fromPosition.offset, toPosition.offset);
- }
- final newSelection = TextSelection(
- baseOffset: baseOffset,
- extentOffset: extentOffset,
- affinity: fromPosition.affinity,
- );
- _handleSelectionChange(newSelection, cause);
- }
- @override
- void selectWord(SelectionChangedCause cause) {
- selectWordsInRange(_lastTapDownPosition!, null, cause);
- }
- @override
- TextSelection selectWordAtPosition(TextPosition position) {
- final child = childAtPosition(position);
- final nodeOffset = child.container.offset;
- final localPosition = TextPosition(
- offset: position.offset - nodeOffset,
- affinity: position.affinity,
- );
- final localWord = child.getWordBoundary(localPosition);
- final 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
- void selectWordEdge(SelectionChangedCause cause) {
- assert(_lastTapDownPosition != null);
- final position = getPositionForOffset(_lastTapDownPosition!);
- final child = childAtPosition(position);
- final nodeOffset = child.container.offset;
- final localPosition = TextPosition(
- offset: position.offset - nodeOffset,
- affinity: position.affinity,
- );
- final localWord = child.getWordBoundary(localPosition);
- final word = TextRange(
- start: localWord.start + nodeOffset,
- end: localWord.end + nodeOffset,
- );
- if (position.offset - word.start <= 1) {
- _handleSelectionChange(
- TextSelection.collapsed(offset: word.start), cause);
- } else {
- _handleSelectionChange(
- TextSelection.collapsed(
- offset: word.end, affinity: TextAffinity.upstream),
- cause,
- );
- }
- }
- @override
- void selectWordsInRange(
- Offset from, Offset? to, SelectionChangedCause cause) {
- final firstPosition = getPositionForOffset(from);
- final firstWord = selectWordAtPosition(firstPosition);
- final lastWord =
- to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to));
- _handleSelectionChange(
- TextSelection(
- baseOffset: firstWord.base.offset,
- extentOffset: lastWord.extent.offset,
- affinity: firstWord.affinity,
- ),
- cause);
- }
- @override
- void paint(PaintingContext context, Offset offset) {
- defaultPaint(context, offset);
- _paintHandleLayers(context, getEndpointsForSelection(_selection));
- }
- @override
- bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
- return defaultHitTestChildren(result, position: position);
- }
- /// Returns the y-offset of the editor at which [selection] is visible.
- ///
- /// The offset is the distance from the top of the editor and is the minimum
- /// from the current scroll position until [selection] becomes visible.
- /// Returns null if [selection] is already visible.
- double? getOffsetToRevealCursor(
- double viewportHeight, double scrollOffset, double offsetInViewport) {
- final endpoints = getEndpointsForSelection(_selection);
- final endpoint = endpoints.first;
- final child = childAtPosition(_selection.extent);
- const kMargin = 8.0;
- final lineHeight = child.preferredLineHeight(
- TextPosition(offset: _selection.extentOffset - child.container.offset),
- );
- final caretTop = endpoint.point.dy -
- lineHeight -
- kMargin +
- offsetInViewport +
- scrollBottomInset;
- final caretBottom =
- endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset;
- double? dy;
- if (caretTop < scrollOffset) {
- dy = caretTop;
- } else if (caretBottom > scrollOffset + viewportHeight) {
- dy = caretBottom - viewportHeight;
- }
- if (dy == null) {
- return null;
- }
- return math.max(dy, 0);
- }
- // Util
- void _handleSelectionChange(
- TextSelection nextSelection, SelectionChangedCause cause) {
- final focusingEmpty = nextSelection.baseOffset == 0 &&
- nextSelection.extentOffset == 0 &&
- !_hasFocus;
- if (nextSelection == _selection &&
- cause != SelectionChangedCause.keyboard &&
- !focusingEmpty) {
- return;
- }
- onSelectionChanged(nextSelection, cause);
- }
- void _paintHandleLayers(
- PaintingContext context, List<TextSelectionPoint> 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,
- );
- }
- }
- }
|