Bring into view upon selection extension

When extending selection, have scroll controller jump to the most appropriate offset to display selection extension
pull/351/head
Xin Yao 4 years ago
parent 30d920fd0f
commit 6f8624e3af
  1. 83
      lib/src/widgets/box.dart
  2. 7
      lib/src/widgets/cursor.dart
  3. 59
      lib/src/widgets/editor.dart
  4. 18
      lib/src/widgets/raw_editor.dart
  5. 55
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  6. 21
      lib/src/widgets/text_block.dart
  7. 21
      lib/src/widgets/text_line.dart

@ -16,24 +16,107 @@ abstract class RenderContentProxyBox implements RenderBox {
List<TextBox> getBoxesForSelection(TextSelection textSelection);
}
/// Base class for render boxes of editable content.
///
/// Implementations of this class usually work as a wrapper around
/// regular (non-editable) render boxes which implement
/// [RenderContentProxyBox].
abstract class RenderEditableBox extends RenderBox {
/// The document node represented by this render box.
Container getContainer();
/// Returns preferred line height at specified `position` in text.
///
/// The `position` parameter must be relative to the [node]'s content.
double preferredLineHeight(TextPosition position);
/// Returns the offset at which to paint the caret.
///
/// The `position` parameter must be relative to the [node]'s content.
///
/// Valid only after [layout].
Offset getOffsetForCaret(TextPosition position);
/// Returns the position within the text for the given pixel offset.
///
/// The `offset` parameter must be local to this box coordinate system.
///
/// Valid only after [layout].
TextPosition getPositionForOffset(Offset offset);
/// Returns the position relative to the [node] content
///
/// The `position` must be within the [node] content
TextPosition globalToLocalPosition(TextPosition position);
/// Returns the position within the text which is on the line above the given
/// `position`.
///
/// The `position` parameter must be relative to the [node] content.
///
/// Primarily used with multi-line or soft-wrapping text.
///
/// Can return `null` which indicates that the `position` is at the topmost
/// line in the text already.
TextPosition? getPositionAbove(TextPosition position);
/// Returns the position within the text which is on the line below the given
/// `position`.
///
/// The `position` parameter must be relative to the [node] content.
///
/// Primarily used with multi-line or soft-wrapping text.
///
/// Can return `null` which indicates that the `position` is at the bottommost
/// line in the text already.
TextPosition? getPositionBelow(TextPosition position);
/// Returns the text range of the word at the given offset. Characters not
/// part of a word, such as spaces, symbols, and punctuation, have word breaks
/// on both sides. In such cases, this method will return a text range that
/// contains the given text position.
///
/// Word boundaries are defined more precisely in Unicode Standard Annex #29
/// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
///
/// The `position` parameter must be relative to the [node]'s content.
///
/// Valid only after [layout].
TextRange getWordBoundary(TextPosition position);
/// Returns the text range of the line at the given offset.
///
/// The newline, if any, is included in the range.
///
/// The `position` parameter must be relative to the [node]'s content.
///
/// Valid only after [layout].
TextRange getLineBoundary(TextPosition position);
/// Returns a list of rects that bound the given selection.
///
/// A given selection might have more than one rect if this text painter
/// contains bidirectional text because logically contiguous text might not be
/// visually contiguous.
///
/// Valid only after [layout].
// List<TextBox> getBoxesForSelection(TextSelection selection);
/// Returns a point for the base selection handle used on touch-oriented
/// devices.
///
/// The `selection` parameter is expected to be in local offsets to this
/// render object's [node].
TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection);
/// Returns a point for the extent selection handle used on touch-oriented
/// devices.
///
/// The `selection` parameter is expected to be in local offsets to this
/// render object's [node].
TextSelectionPoint getExtentEndpointForSelection(TextSelection textSelection);
/// Returns the [Rect] in local coordinates for the caret at the given text
/// position.
Rect getLocalRectForCaret(TextPosition position);
}

@ -292,8 +292,7 @@ class CursorPainter {
}
}
final pixelPerfectOffset =
_getPixelPerfectCursorOffset(editable!, caretRect, devicePixelRatio);
final pixelPerfectOffset = _getPixelPerfectCursorOffset(caretRect);
if (!pixelPerfectOffset.isFinite) {
return;
}
@ -309,11 +308,9 @@ class CursorPainter {
}
Offset _getPixelPerfectCursorOffset(
RenderContentProxyBox editable,
Rect caretRect,
double devicePixelRatio,
) {
final caretPosition = editable.localToGlobal(caretRect.topLeft);
final caretPosition = editable!.localToGlobal(caretRect.topLeft);
final pixelMultiple = 1.0 / devicePixelRatio;
final pixelPerfectOffsetX = caretPosition.dx.isFinite

@ -47,6 +47,8 @@ const linkPrefixes = [
];
abstract class EditorState extends State<RawEditor> {
ScrollController get scrollController;
TextEditingValue getTextEditingValue();
void setTextEditingValue(TextEditingValue value);
@ -62,32 +64,75 @@ abstract class EditorState extends State<RawEditor> {
void requestKeyboard();
}
/// Base interface for editable render objects.
abstract class RenderAbstractEditor {
TextSelection selectWordAtPosition(TextPosition position);
TextSelection selectLineAtPosition(TextPosition position);
/// Returns preferred line height at specified `position` in text.
double preferredLineHeight(TextPosition position);
/// Returns [Rect] for caret in local coordinates
///
/// Useful to enforce visibility of full caret at given position
Rect getLocalRectForCaret(TextPosition position);
/// Returns the local coordinates of the endpoints of the given selection.
///
/// If the selection is collapsed (and therefore occupies a single point), the
/// returned list is of length one. Otherwise, the selection is not collapsed
/// and the returned list is of length two. In this case, however, the two
/// points might actually be co-located (e.g., because of a bidirectional
/// selection that contains some text but whose ends meet in the middle).
TextPosition getPositionForOffset(Offset offset);
List<TextSelectionPoint> getEndpointsForSelection(
TextSelection textSelection);
/// If [ignorePointer] is false (the default) then this method is called by
/// the internal gesture recognizer's [TapGestureRecognizer.onTapDown]
/// callback.
///
/// When [ignorePointer] is true, an ancestor widget must respond to tap
/// down events by calling this method.
void handleTapDown(TapDownDetails details);
/// Selects the set words of a paragraph in a given range of global positions.
///
/// The first and last endpoints of the selection will always be at the
/// beginning and end of a word respectively.
///
/// {@macro flutter.rendering.editable.select}
void selectWordsInRange(
Offset from,
Offset to,
SelectionChangedCause cause,
);
/// Move the selection to the beginning or end of a word.
///
/// {@macro flutter.rendering.editable.select}
void selectWordEdge(SelectionChangedCause cause);
/// Select text between the global positions [from] and [to].
void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause);
/// Select a word around the location of the last tap down.
///
/// {@macro flutter.rendering.editable.select}
void selectWord(SelectionChangedCause cause);
/// Move selection to the location of the last tap down.
///
/// {@template flutter.rendering.editable.select}
/// This method is mainly used to translate user inputs in global positions
/// into a [TextSelection]. When used in conjunction with a [EditableText],
/// the selection change is fed back into [TextEditingController.selection].
///
/// If you have a [TextEditingController], it's generally easier to
/// programmatically manipulate its `value` or `selection` directly.
/// {@endtemplate}
void selectPosition(SelectionChangedCause cause);
}
@ -988,7 +1033,8 @@ class RenderEditor extends RenderEditableContainerBox
final caretTop = endpoint.point.dy -
child.preferredLineHeight(TextPosition(
offset: selection.extentOffset - child.getContainer().offset)) -
offset:
selection.extentOffset - child.getContainer().documentOffset)) -
kMargin +
offsetInViewport +
scrollBottomInset;
@ -1005,6 +1051,17 @@ class RenderEditor extends RenderEditableContainerBox
}
return math.max(dy, 0);
}
@override
Rect getLocalRectForCaret(TextPosition position) {
final targetChild = childAtPosition(position);
final localPosition = targetChild.globalToLocalPosition(position);
final childLocalRect = targetChild.getLocalRectForCaret(localPosition);
final boxParentData = targetChild.parentData as BoxParentData;
return childLocalRect.shift(Offset(0, boxParentData.offset.dy));
}
}
class EditableContainerParentData

@ -115,7 +115,9 @@ class RawEditorState extends EditorState
EditorTextSelectionOverlay? getSelectionOverlay() => _selectionOverlay;
EditorTextSelectionOverlay? _selectionOverlay;
ScrollController? _scrollController;
@override
ScrollController get scrollController => _scrollController;
late ScrollController _scrollController;
late CursorCont _cursorCont;
@ -323,7 +325,7 @@ class RawEditorState extends EditorState
});
_scrollController = widget.scrollController;
_scrollController!.addListener(_updateSelectionOverlayForScroll);
_scrollController.addListener(_updateSelectionOverlayForScroll);
_cursorCont = CursorCont(
show: ValueNotifier<bool>(widget.showCursor),
@ -392,9 +394,9 @@ class RawEditorState extends EditorState
}
if (widget.scrollController != _scrollController) {
_scrollController!.removeListener(_updateSelectionOverlayForScroll);
_scrollController.removeListener(_updateSelectionOverlayForScroll);
_scrollController = widget.scrollController;
_scrollController!.addListener(_updateSelectionOverlayForScroll);
_scrollController.addListener(_updateSelectionOverlayForScroll);
}
if (widget.focusNode != oldWidget.focusNode) {
@ -570,16 +572,16 @@ class RawEditorState extends EditorState
final viewport = RenderAbstractViewport.of(renderEditor);
final editorOffset =
renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport);
final offsetInViewport = _scrollController!.offset + editorOffset.dy;
final offsetInViewport = _scrollController.offset + editorOffset.dy;
final offset = renderEditor.getOffsetToRevealCursor(
_scrollController!.position.viewportDimension,
_scrollController!.offset,
_scrollController.position.viewportDimension,
_scrollController.offset,
offsetInViewport,
);
if (offset != null) {
_scrollController!.animateTo(
_scrollController.animateTo(
offset,
duration: const Duration(milliseconds: 100),
curve: Curves.fastOutSlowIn,

@ -1,3 +1,6 @@
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../editor.dart';
@ -16,7 +19,57 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
@override
void bringIntoView(TextPosition position) {
// TODO: implement bringIntoView
final localRect = getRenderEditor()!.getLocalRectForCaret(position);
final targetOffset = _getOffsetToRevealCaret(localRect, position);
scrollController.jumpTo(targetOffset.offset);
getRenderEditor()!.showOnScreen(rect: targetOffset.rect);
}
// Finds the closest scroll offset to the current scroll offset that fully
// reveals the given caret rect. If the given rect's main axis extent is too
// large to be fully revealed in `renderEditable`, it will be centered along
// the main axis.
//
// If this is a multiline EditableText (which means the Editable can only
// scroll vertically), the given rect's height will first be extended to match
// `renderEditable.preferredLineHeight`, before the target scroll offset is
// calculated.
RevealedOffset _getOffsetToRevealCaret(Rect rect, TextPosition position) {
if (!scrollController.position.allowImplicitScrolling) {
return RevealedOffset(offset: scrollController.offset, rect: rect);
}
final editableSize = getRenderEditor()!.size;
final double additionalOffset;
final Offset unitOffset;
// The caret is vertically centered within the line. Expand the caret's
// height so that it spans the line because we're going to ensure that the
// entire expanded caret is scrolled into view.
final expandedRect = Rect.fromCenter(
center: rect.center,
width: rect.width,
height:
max(rect.height, getRenderEditor()!.preferredLineHeight(position)),
);
additionalOffset = expandedRect.height >= editableSize.height
? editableSize.height / 2 - expandedRect.center.dy
: 0.0
.clamp(expandedRect.bottom - editableSize.height, expandedRect.top);
unitOffset = const Offset(0, 1);
// No overscrolling when encountering tall fonts/scripts that extend past
// the ascent.
final targetOffset = (additionalOffset + scrollController.offset).clamp(
scrollController.position.minScrollExtent,
scrollController.position.maxScrollExtent,
);
final offsetDelta = scrollController.offset - targetOffset;
return RevealedOffset(
rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
}
@override

@ -527,6 +527,27 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
@override
Rect getLocalRectForCaret(TextPosition position) {
final child = childAtPosition(position);
final localPosition = TextPosition(
offset: position.offset - child.getContainer().offset,
affinity: position.affinity,
);
final parentData = child.parentData as BoxParentData;
return child.getLocalRectForCaret(localPosition).shift(parentData.offset);
}
@override
TextPosition globalToLocalPosition(TextPosition position) {
assert(getContainer().containsOffset(position.offset),
'The provided text position is not in the current node');
return TextPosition(
offset: position.offset - getContainer().documentOffset,
affinity: position.affinity,
);
}
}
class _EditableBlock extends MultiChildRenderObjectWidget {

@ -831,6 +831,27 @@ class RenderEditableTextLine extends RenderEditableBox {
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return _children.first.hitTest(result, position: position);
}
@override
Rect getLocalRectForCaret(TextPosition position) {
final caretOffset = getOffsetForCaret(position);
var rect =
Rect.fromLTWH(0, 0, cursorWidth, cursorHeight).shift(caretOffset);
final cursorOffset = cursorCont.style.offset;
// Add additional cursor offset (generally only if on iOS).
if (cursorOffset != null) rect = rect.shift(cursorOffset);
return rect;
}
@override
TextPosition globalToLocalPosition(TextPosition position) {
assert(getContainer().containsOffset(position.offset),
'The provided text position is not in the current node');
return TextPosition(
offset: position.offset - getContainer().documentOffset,
affinity: position.affinity,
);
}
}
class _TextLineElement extends RenderObjectElement {

Loading…
Cancel
Save