Commit 527fddc6 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Port EditableText to Scrollable2 (#8167)

parent 3985ddbc
...@@ -28,7 +28,7 @@ export 'src/rendering/block.dart'; ...@@ -28,7 +28,7 @@ export 'src/rendering/block.dart';
export 'src/rendering/box.dart'; export 'src/rendering/box.dart';
export 'src/rendering/custom_layout.dart'; export 'src/rendering/custom_layout.dart';
export 'src/rendering/debug.dart'; export 'src/rendering/debug.dart';
export 'src/rendering/editable_line.dart'; export 'src/rendering/editable.dart';
export 'src/rendering/error.dart'; export 'src/rendering/error.dart';
export 'src/rendering/flex.dart'; export 'src/rendering/flex.dart';
export 'src/rendering/flow.dart'; export 'src/rendering/flow.dart';
......
...@@ -147,7 +147,6 @@ class _InputFieldState extends State<InputField> { ...@@ -147,7 +147,6 @@ class _InputFieldState extends State<InputField> {
cursorColor: themeData.textSelectionColor, cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor, selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls, selectionControls: materialTextSelectionControls,
platform: Theme.of(context).platform,
keyboardType: config.keyboardType, keyboardType: config.keyboardType,
onChanged: config.onChanged, onChanged: config.onChanged,
onSubmitted: config.onSubmitted, onSubmitted: config.onSubmitted,
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui show TextBox; import 'dart:ui' as ui show TextBox;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'viewport.dart'; import 'viewport_offset.dart';
const double _kCaretGap = 1.0; // pixels const double _kCaretGap = 1.0; // pixels
const double _kCaretHeightOffset = 2.0; // pixels const double _kCaretHeightOffset = 2.0; // pixels
...@@ -39,14 +41,6 @@ class TextSelectionPoint { ...@@ -39,14 +41,6 @@ class TextSelectionPoint {
final TextDirection direction; final TextDirection direction;
} }
/// Signature for the callback used by [RenderEditable] to determine the paint offset when
/// the dimensions of the render box change.
///
/// The return value should be the new paint offset to use.
///
/// Used by [RenderEditable.onPaintOffsetUpdateNeeded].
typedef Offset RenderEditablePaintOffsetNeededCallback(ViewportDimensions dimensions, Rect caretRect);
/// A single line of editable text. /// A single line of editable text.
class RenderEditable extends RenderBox { class RenderEditable extends RenderBox {
/// Creates a render object for a single line of editable text. /// Creates a render object for a single line of editable text.
...@@ -58,15 +52,18 @@ class RenderEditable extends RenderBox { ...@@ -58,15 +52,18 @@ class RenderEditable extends RenderBox {
Color selectionColor, Color selectionColor,
double textScaleFactor: 1.0, double textScaleFactor: 1.0,
TextSelection selection, TextSelection selection,
@required ViewportOffset offset,
this.onSelectionChanged, this.onSelectionChanged,
Offset paintOffset: Offset.zero,
this.onPaintOffsetUpdateNeeded,
}) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor), }) : _textPainter = new TextPainter(text: text, textScaleFactor: textScaleFactor),
_cursorColor = cursorColor, _cursorColor = cursorColor,
_showCursor = showCursor, _showCursor = showCursor,
_maxLines = maxLines, _maxLines = maxLines,
_selection = selection, _selection = selection,
_paintOffset = paintOffset { _offset = offset {
assert(showCursor != null);
assert(maxLines != null);
assert(textScaleFactor != null);
assert(offset != null);
assert(!showCursor || cursorColor != null); assert(!showCursor || cursorColor != null);
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
...@@ -79,9 +76,6 @@ class RenderEditable extends RenderBox { ...@@ -79,9 +76,6 @@ class RenderEditable extends RenderBox {
/// Called when the selection changes. /// Called when the selection changes.
SelectionChangedHandler onSelectionChanged; SelectionChangedHandler onSelectionChanged;
/// Called when the inner or outer dimensions of this render object change.
RenderEditablePaintOffsetNeededCallback onPaintOffsetUpdateNeeded;
/// The text to display /// The text to display
TextSpan get text => _textPainter.text; TextSpan get text => _textPainter.text;
final TextPainter _textPainter; final TextPainter _textPainter;
...@@ -106,6 +100,7 @@ class RenderEditable extends RenderBox { ...@@ -106,6 +100,7 @@ class RenderEditable extends RenderBox {
bool get showCursor => _showCursor; bool get showCursor => _showCursor;
bool _showCursor; bool _showCursor;
set showCursor(bool value) { set showCursor(bool value) {
assert(value != null);
if (_showCursor == value) if (_showCursor == value)
return; return;
_showCursor = value; _showCursor = value;
...@@ -118,6 +113,7 @@ class RenderEditable extends RenderBox { ...@@ -118,6 +113,7 @@ class RenderEditable extends RenderBox {
int get maxLines => _maxLines; int get maxLines => _maxLines;
int _maxLines; int _maxLines;
set maxLines(int value) { set maxLines(int value) {
assert(value != null);
if (_maxLines == value) if (_maxLines == value)
return; return;
_maxLines = value; _maxLines = value;
...@@ -165,15 +161,58 @@ class RenderEditable extends RenderBox { ...@@ -165,15 +161,58 @@ class RenderEditable extends RenderBox {
/// If the text content is larger than the editable line itself, the editable /// If the text content is larger than the editable line itself, the editable
/// line clips the text. This property controls which part of the text is /// line clips the text. This property controls which part of the text is
/// visible by shifting the text by the given offset before clipping. /// visible by shifting the text by the given offset before clipping.
Offset get paintOffset => _paintOffset; ViewportOffset get offset => _offset;
Offset _paintOffset; ViewportOffset _offset;
set paintOffset(Offset value) { set offset(ViewportOffset value) {
if (_paintOffset == value) assert(value != null);
if (_offset == value)
return; return;
_paintOffset = value; if (attached)
markNeedsPaint(); _offset.removeListener(markNeedsPaint);
_offset = value;
if (attached)
_offset.addListener(markNeedsPaint);
markNeedsLayout();
}
bool get _isMultiline => maxLines > 1;
Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal;
Offset get _paintOffset {
switch (_viewportAxis) {
case Axis.horizontal:
return new Offset(-offset.pixels, 0.0);
case Axis.vertical:
return new Offset(0.0, -offset.pixels);
}
return null;
} }
double get _viewportExtent {
assert(hasSize);
switch (_viewportAxis) {
case Axis.horizontal:
return size.width;
case Axis.vertical:
return size.height;
}
return null;
}
double _getMaxScrollExtent(Size contentSize) {
assert(hasSize);
switch (_viewportAxis) {
case Axis.horizontal:
return math.max(0.0, contentSize.width - size.width);
case Axis.vertical:
return math.max(0.0, contentSize.height - size.height);
}
return null;
}
bool _hasVisualOverflow = false;
/// Returns the global coordinates of the endpoints of the given selection. /// Returns the global coordinates of the endpoints of the given selection.
/// ///
/// If the selection is collapsed (and therefore occupies a single point), the /// If the selection is collapsed (and therefore occupies a single point), the
...@@ -187,17 +226,17 @@ class RenderEditable extends RenderBox { ...@@ -187,17 +226,17 @@ class RenderEditable extends RenderBox {
// clean at this point. // clean at this point.
_textPainter.layout(maxWidth: _maxContentWidth); _textPainter.layout(maxWidth: _maxContentWidth);
Offset offset = _paintOffset; final Offset paintOffset = _paintOffset;
if (selection.isCollapsed) { if (selection.isCollapsed) {
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary. // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
Point start = new Point(0.0, _preferredLineHeight) + caretOffset + offset; final Point start = new Point(0.0, _preferredLineHeight) + caretOffset + paintOffset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)]; return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
} else { } else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection); final List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
Point start = new Point(boxes.first.start, boxes.first.bottom) + offset; final Point start = new Point(boxes.first.start, boxes.first.bottom) + paintOffset;
Point end = new Point(boxes.last.end, boxes.last.bottom) + offset; final Point end = new Point(boxes.last.end, boxes.last.bottom) + paintOffset;
return <TextSelectionPoint>[ return <TextSelectionPoint>[
new TextSelectionPoint(localToGlobal(start), boxes.first.direction), new TextSelectionPoint(localToGlobal(start), boxes.first.direction),
new TextSelectionPoint(localToGlobal(end), boxes.last.direction), new TextSelectionPoint(localToGlobal(end), boxes.last.direction),
...@@ -207,26 +246,24 @@ class RenderEditable extends RenderBox { ...@@ -207,26 +246,24 @@ class RenderEditable extends RenderBox {
/// Returns the position in the text for the given global coordinate. /// Returns the position in the text for the given global coordinate.
TextPosition getPositionForPoint(Point globalPosition) { TextPosition getPositionForPoint(Point globalPosition) {
globalPosition += -paintOffset; globalPosition += -_paintOffset;
return _textPainter.getPositionForOffset(globalToLocal(globalPosition).toOffset()); return _textPainter.getPositionForOffset(globalToLocal(globalPosition).toOffset());
} }
/// Returns the Rect in local coordinates for the caret at the given text /// Returns the Rect in local coordinates for the caret at the given text
/// position. /// position.
Rect getLocalRectForCaret(TextPosition caretPosition) { Rect getLocalRectForCaret(TextPosition caretPosition) {
Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding. // This rect is the same as _caretPrototype but without the vertical padding.
return new Rect.fromLTWH(0.0, 0.0, _kCaretWidth, _preferredLineHeight).shift(caretOffset + _paintOffset); return new Rect.fromLTWH(0.0, 0.0, _kCaretWidth, _preferredLineHeight).shift(caretOffset + _paintOffset);
} }
Size _contentSize;
double get _preferredLineHeight => _textPainter.preferredLineHeight; double get _preferredLineHeight => _textPainter.preferredLineHeight;
double get _maxContentWidth { double get _maxContentWidth {
return _maxLines > 1 ? if (_maxLines > 1)
constraints.maxWidth - (_kCaretGap + _kCaretWidth) : return constraints.maxWidth - (_kCaretGap + _kCaretWidth);
double.INFINITY; return double.INFINITY;
} }
@override @override
...@@ -257,15 +294,15 @@ class RenderEditable extends RenderBox { ...@@ -257,15 +294,15 @@ class RenderEditable extends RenderBox {
Point _lastTapDownPosition; Point _lastTapDownPosition;
Point _longPressPosition; Point _longPressPosition;
void _handleTapDown(TapDownDetails details) { void _handleTapDown(TapDownDetails details) {
_lastTapDownPosition = details.globalPosition + -paintOffset; _lastTapDownPosition = details.globalPosition + -_paintOffset;
} }
void _handleTap() { void _handleTap() {
assert(_lastTapDownPosition != null); assert(_lastTapDownPosition != null);
final Point global = _lastTapDownPosition; final Point globalPosition = _lastTapDownPosition;
_lastTapDownPosition = null; _lastTapDownPosition = null;
if (onSelectionChanged != null) { if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset()); TextPosition position = _textPainter.getPositionForOffset(globalToLocal(globalPosition).toOffset());
onSelectionChanged(new TextSelection.fromPosition(position), this, false); onSelectionChanged(new TextSelection.fromPosition(position), this, false);
} }
} }
...@@ -277,16 +314,16 @@ class RenderEditable extends RenderBox { ...@@ -277,16 +314,16 @@ class RenderEditable extends RenderBox {
} }
void _handleLongPress() { void _handleLongPress() {
final Point global = _longPressPosition; final Point globalPosition = _longPressPosition;
_longPressPosition = null; _longPressPosition = null;
if (onSelectionChanged != null) { if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset()); final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(globalPosition).toOffset());
onSelectionChanged(_selectWordAtOffset(position), this, true); onSelectionChanged(_selectWordAtOffset(position), this, true);
} }
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _selectWordAtOffset(TextPosition position) {
TextRange word = _textPainter.getWordBoundary(position); final TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor. // When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end) if (position.offset >= word.end)
return new TextSelection.fromPosition(position); return new TextSelection.fromPosition(position);
...@@ -297,41 +334,34 @@ class RenderEditable extends RenderBox { ...@@ -297,41 +334,34 @@ class RenderEditable extends RenderBox {
@override @override
void performLayout() { void performLayout() {
Size oldSize = hasSize ? size : null;
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset);
_selectionRects = null; _selectionRects = null;
_textPainter.layout(maxWidth: _maxContentWidth); _textPainter.layout(maxWidth: _maxContentWidth);
size = new Size(constraints.maxWidth, constraints.constrainHeight( size = new Size(constraints.maxWidth, constraints.constrainHeight(
_textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines) _textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines)
)); ));
Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); final Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
assert(_selection != null); final double _maxScrollExtent = _getMaxScrollExtent(contentSize);
Rect caretRect = getLocalRectForCaret(_selection.extent); _hasVisualOverflow = _maxScrollExtent > 0.0;
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize || !_withinBounds(caretRect))) offset.applyViewportDimension(_viewportExtent);
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize), caretRect); offset.applyContentDimensions(0.0, _maxScrollExtent);
_contentSize = contentSize;
}
bool _withinBounds(Rect caretRect) {
Rect bounds = new Rect.fromLTWH(0.0, 0.0, size.width, size.height);
return (bounds.contains(caretRect.topLeft) && bounds.contains(caretRect.bottomRight));
} }
void _paintCaret(Canvas canvas, Offset effectiveOffset) { void _paintCaret(Canvas canvas, Offset effectiveOffset) {
Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
Paint paint = new Paint()..color = _cursorColor; final Paint paint = new Paint()..color = _cursorColor;
canvas.drawRect(_caretPrototype.shift(caretOffset + effectiveOffset), paint); canvas.drawRect(_caretPrototype.shift(caretOffset + effectiveOffset), paint);
} }
void _paintSelection(Canvas canvas, Offset effectiveOffset) { void _paintSelection(Canvas canvas, Offset effectiveOffset) {
assert(_selectionRects != null); assert(_selectionRects != null);
Paint paint = new Paint()..color = _selectionColor; final Paint paint = new Paint()..color = _selectionColor;
for (ui.TextBox box in _selectionRects) for (ui.TextBox box in _selectionRects)
canvas.drawRect(box.toRect().shift(effectiveOffset), paint); canvas.drawRect(box.toRect().shift(effectiveOffset), paint);
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
if (_selection != null) { if (_selection != null) {
if (_selection.isCollapsed && _showCursor && cursorColor != null) { if (_selection.isCollapsed && _showCursor && cursorColor != null) {
...@@ -345,8 +375,6 @@ class RenderEditable extends RenderBox { ...@@ -345,8 +375,6 @@ class RenderEditable extends RenderBox {
_textPainter.paint(context.canvas, effectiveOffset); _textPainter.paint(context.canvas, effectiveOffset);
} }
bool get _hasVisualOverflow => _contentSize.width > size.width;
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow) if (_hasVisualOverflow)
...@@ -357,4 +385,24 @@ class RenderEditable extends RenderBox { ...@@ -357,4 +385,24 @@ class RenderEditable extends RenderBox {
@override @override
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null; Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('cursorColor: $_cursorColor');
description.add('showCursor: $_showCursor');
description.add('maxLines: $_maxLines');
description.add('selectionColor: $_selectionColor');
description.add('textScaleFactor: $textScaleFactor');
description.add('selection: $_selection');
description.add('offset: $_offset');
}
@override
String debugDescribeChildren(String prefix) {
return '$prefix \u2558\u2550\u2566\u2550\u2550 text \u2550\u2550\u2550\n'
'${text.toString("$prefix \u2551 ")}' // TextSpan includes a newline
'$prefix \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n'
'$prefix\n';
}
} }
...@@ -5,14 +5,15 @@ ...@@ -5,14 +5,15 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart' show RenderEditable, SelectionChangedHandler, RenderEditablePaintOffsetNeededCallback; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus.dart'; import 'focus.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'scroll_behavior.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'text_selection.dart'; import 'text_selection.dart';
...@@ -135,32 +136,33 @@ class InputValue { ...@@ -135,32 +136,33 @@ class InputValue {
/// * [InputField], which adds tap-to-focus and cut, copy, and paste commands. /// * [InputField], which adds tap-to-focus and cut, copy, and paste commands.
/// * [TextField], which is a full-featured, material-design text input field /// * [TextField], which is a full-featured, material-design text input field
/// with placeholder text, labels, and [Form] integration. /// with placeholder text, labels, and [Form] integration.
class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE class EditableText extends StatefulWidget {
/// Creates a basic text input control. /// Creates a basic text input control.
/// ///
/// The [value] argument must not be null. /// The [value] argument must not be null.
EditableText({ EditableText({
Key key, Key key,
@required this.value, @required this.value,
this.focusKey, @required this.focusKey,
this.obscureText: false, this.obscureText: false,
this.style, @required this.style,
this.cursorColor, @required this.cursorColor,
this.textScaleFactor, this.textScaleFactor,
int maxLines: 1, this.maxLines: 1,
this.autofocus: false, this.autofocus: false,
this.selectionColor, this.selectionColor,
this.selectionControls, this.selectionControls,
@required this.platform,
this.keyboardType, this.keyboardType,
this.onChanged, this.onChanged,
this.onSubmitted this.onSubmitted,
}) : maxLines = maxLines, super( }) : super(key: key) {
key: key,
initialScrollOffset: 0.0,
scrollDirection: maxLines > 1 ? Axis.vertical : Axis.horizontal
) {
assert(value != null); assert(value != null);
assert(focusKey != null);
assert(obscureText != null);
assert(style != null);
assert(cursorColor != null);
assert(maxLines != null);
assert(autofocus != null);
} }
/// The string being displayed in this widget. /// The string being displayed in this widget.
...@@ -206,12 +208,6 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE ...@@ -206,12 +208,6 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE
/// Optional delegate for building the text selection handles and toolbar. /// Optional delegate for building the text selection handles and toolbar.
final TextSelectionControls selectionControls; final TextSelectionControls selectionControls;
/// The platform whose behavior should be approximated, in particular
/// for scroll physics. (See [ScrollBehavior.platform].)
///
/// Must not be null.
final TargetPlatform platform;
/// The type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
final TextInputType keyboardType; final TextInputType keyboardType;
...@@ -226,7 +222,7 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE ...@@ -226,7 +222,7 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE
} }
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends ScrollableState<EditableText> implements TextInputClient { // ignore: DEPRECATED_MEMBER_USE class EditableTextState extends State<EditableText> implements TextInputClient {
Timer _cursorTimer; Timer _cursorTimer;
bool _showCursor = false; bool _showCursor = false;
...@@ -234,11 +230,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -234,11 +230,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
TextInputConnection _textInputConnection; TextInputConnection _textInputConnection;
TextSelectionOverlay _selectionOverlay; TextSelectionOverlay _selectionOverlay;
@override final ScrollController _scrollController = new ScrollController();
ExtentScrollBehavior createScrollBehavior() => new BoundedBehavior(platform: config.platform);
@override
BoundedBehavior get scrollBehavior => super.scrollBehavior;
@override @override
void initState() { void initState() {
...@@ -259,41 +251,17 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -259,41 +251,17 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
bool get _isMultiline => config.maxLines > 1; bool get _isMultiline => config.maxLines > 1;
double _contentExtent = 0.0;
double _containerExtent = 0.0;
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions, Rect caretRect) {
// We make various state changes here but don't have to do so in a
// setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the
// render object via our return value.
_contentExtent = _isMultiline ?
dimensions.contentSize.height :
dimensions.contentSize.width;
_containerExtent = _isMultiline ?
dimensions.containerSize.height :
dimensions.containerSize.width;
didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: _contentExtent,
containerExtent: _containerExtent,
// TODO(ianh): We should really only do this when text is added,
// not generally any time the size changes.
scrollOffset: _getScrollOffsetForCaret(caretRect, _containerExtent)
));
updateGestureDetector();
return scrollOffsetToPixelDelta(scrollOffset);
}
// Calculate the new scroll offset so the cursor remains visible. // Calculate the new scroll offset so the cursor remains visible.
double _getScrollOffsetForCaret(Rect caretRect, double containerExtent) { double _getScrollOffsetForCaret(Rect caretRect) {
double caretStart = _isMultiline ? caretRect.top : caretRect.left; final double caretStart = _isMultiline ? caretRect.top : caretRect.left;
double caretEnd = _isMultiline ? caretRect.bottom : caretRect.right; final double caretEnd = _isMultiline ? caretRect.bottom : caretRect.right;
double newScrollOffset = scrollOffset; double scrollOffset = _scrollController.offset;
final double viewportExtent = _scrollController.position.viewportDimension;
if (caretStart < 0.0) // cursor before start of bounds if (caretStart < 0.0) // cursor before start of bounds
newScrollOffset += pixelOffsetToScrollOffset(-caretStart); scrollOffset += caretStart;
else if (caretEnd >= containerExtent) // cursor after end of bounds else if (caretEnd >= viewportExtent) // cursor after end of bounds
newScrollOffset += pixelOffsetToScrollOffset(-(caretEnd - containerExtent)); scrollOffset += caretEnd - viewportExtent;
return newScrollOffset; return scrollOffset;
} }
// True if the focus was explicitly requested last frame. This ensures we // True if the focus was explicitly requested last frame. This ensures we
...@@ -302,8 +270,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -302,8 +270,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
void _attachOrDetachKeyboard(bool focused) { void _attachOrDetachKeyboard(bool focused) {
if (focused && !_isAttachedToKeyboard && (_requestingFocus || config.autofocus)) { if (focused && !_isAttachedToKeyboard && (_requestingFocus || config.autofocus)) {
_textInputConnection = TextInput.attach( _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
this, new TextInputConfiguration(inputType: config.keyboardType))
..setEditingState(_getTextEditingStateFromInputValue(_currentValue)) ..setEditingState(_getTextEditingStateFromInputValue(_currentValue))
..show(); ..show();
} else if (!focused) { } else if (!focused) {
...@@ -364,7 +331,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -364,7 +331,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
// EditableWidget, not just changes triggered by user gestures. // EditableWidget, not just changes triggered by user gestures.
requestKeyboard(); requestKeyboard();
InputValue newInput = _currentValue.copyWith(selection: selection, composing: TextRange.empty); final InputValue newInput = _currentValue.copyWith(selection: selection, composing: TextRange.empty);
if (config.onChanged != null) if (config.onChanged != null)
config.onChanged(newInput); config.onChanged(newInput);
...@@ -393,14 +360,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -393,14 +360,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
assert(!newInput.composing.isValid); // composing range must be empty while selecting assert(!newInput.composing.isValid); // composing range must be empty while selecting
if (config.onChanged != null) if (config.onChanged != null)
config.onChanged(newInput); config.onChanged(newInput);
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
didUpdateScrollBehavior(scrollBehavior.updateExtents(
// TODO(mpcomplete): should just be able to pass
// scrollBehavior.containerExtent here (and remove the member var), but
// scrollBehavior gets re-created too often, and is sometimes
// uninitialized here. Investigate if this is a bug.
scrollOffset: _getScrollOffsetForCaret(caretRect, _containerExtent)
));
} }
/// Whether the blinking cursor is actually visible at this precise moment /// Whether the blinking cursor is actually visible at this precise moment
...@@ -429,9 +389,12 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -429,9 +389,12 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
_textInputConnection.close(); _textInputConnection.close();
_textInputConnection = null; _textInputConnection = null;
} }
assert(!_isAttachedToKeyboard);
if (_cursorTimer != null) if (_cursorTimer != null)
_stopCursorTimer(); _stopCursorTimer();
assert(_cursorTimer == null);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null;
super.dispose(); super.dispose();
} }
...@@ -442,11 +405,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -442,11 +405,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
} }
@override @override
Widget buildContent(BuildContext context) { Widget build(BuildContext context) {
assert(config.style != null);
assert(config.focusKey != null);
assert(config.cursorColor != null);
bool focused = Focus.at(config.focusKey.currentContext); bool focused = Focus.at(config.focusKey.currentContext);
_attachOrDetachKeyboard(focused); _attachOrDetachKeyboard(focused);
...@@ -464,8 +423,12 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -464,8 +423,12 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
} }
} }
return new ClipRect( return new Scrollable2(
child: new _Editable( axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController,
physics: const ClampingScrollPhysics(),
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new _Editable(
value: _currentValue, value: _currentValue,
style: config.style, style: config.style,
cursorColor: config.cursorColor, cursorColor: config.cursorColor,
...@@ -474,10 +437,10 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp ...@@ -474,10 +437,10 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
selectionColor: config.selectionColor, selectionColor: config.selectionColor,
textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor, textScaleFactor: config.textScaleFactor ?? MediaQuery.of(context).textScaleFactor,
obscureText: config.obscureText, obscureText: config.obscureText,
offset: offset,
onSelectionChanged: _handleSelectionChanged, onSelectionChanged: _handleSelectionChanged,
paintOffset: scrollOffsetToPixelDelta(scrollOffset), );
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded },
)
); );
} }
} }
...@@ -493,9 +456,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -493,9 +456,8 @@ class _Editable extends LeafRenderObjectWidget {
this.selectionColor, this.selectionColor,
this.textScaleFactor, this.textScaleFactor,
this.obscureText, this.obscureText,
this.offset,
this.onSelectionChanged, this.onSelectionChanged,
this.paintOffset,
this.onPaintOffsetUpdateNeeded
}) : super(key: key); }) : super(key: key);
final InputValue value; final InputValue value;
...@@ -506,9 +468,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -506,9 +468,8 @@ class _Editable extends LeafRenderObjectWidget {
final Color selectionColor; final Color selectionColor;
final double textScaleFactor; final double textScaleFactor;
final bool obscureText; final bool obscureText;
final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged; final SelectionChangedHandler onSelectionChanged;
final Offset paintOffset;
final RenderEditablePaintOffsetNeededCallback onPaintOffsetUpdateNeeded;
@override @override
RenderEditable createRenderObject(BuildContext context) { RenderEditable createRenderObject(BuildContext context) {
...@@ -520,9 +481,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -520,9 +481,8 @@ class _Editable extends LeafRenderObjectWidget {
selectionColor: selectionColor, selectionColor: selectionColor,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
selection: value.selection, selection: value.selection,
offset: offset,
onSelectionChanged: onSelectionChanged, onSelectionChanged: onSelectionChanged,
paintOffset: paintOffset,
onPaintOffsetUpdateNeeded: onPaintOffsetUpdateNeeded
); );
} }
...@@ -536,9 +496,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -536,9 +496,8 @@ class _Editable extends LeafRenderObjectWidget {
..selectionColor = selectionColor ..selectionColor = selectionColor
..textScaleFactor = textScaleFactor ..textScaleFactor = textScaleFactor
..selection = value.selection ..selection = value.selection
..onSelectionChanged = onSelectionChanged ..offset = offset
..paintOffset = paintOffset ..onSelectionChanged = onSelectionChanged;
..onPaintOffsetUpdateNeeded = onPaintOffsetUpdateNeeded;
} }
TextSpan get _styledTextSpan { TextSpan get _styledTextSpan {
......
...@@ -184,13 +184,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -184,13 +184,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
@override @override
bool get isRepaintBoundary => true; bool get isRepaintBoundary => true;
double get _effectiveExtent { double get _viewportExtent {
assert(hasSize); assert(hasSize);
switch (axis) { switch (axis) {
case Axis.vertical:
return size.height;
case Axis.horizontal: case Axis.horizontal:
return size.width; return size.width;
case Axis.vertical:
return size.height;
} }
return null; return null;
} }
...@@ -265,7 +265,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -265,7 +265,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
size = constraints.constrain(child.size); size = constraints.constrain(child.size);
} }
offset.applyViewportDimension(_effectiveExtent); offset.applyViewportDimension(_viewportExtent);
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
} }
......
...@@ -581,7 +581,6 @@ void main() { ...@@ -581,7 +581,6 @@ void main() {
expect(inputValue.text, cutValue); expect(inputValue.text, cutValue);
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961 }, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
testWidgets('Can scroll multiline input', (WidgetTester tester) async { testWidgets('Can scroll multiline input', (WidgetTester tester) async {
GlobalKey inputKey = new GlobalKey(); GlobalKey inputKey = new GlobalKey();
InputValue inputValue = InputValue.empty; InputValue inputValue = InputValue.empty;
...@@ -640,6 +639,7 @@ void main() { ...@@ -640,6 +639,7 @@ void main() {
// Now the first line is scrolled up, and the fourth line is visible. // Now the first line is scrolled up, and the fourth line is visible.
Point newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First')); Point newFirstPos = textOffsetToPosition(tester, kFourLines.indexOf('First'));
Point newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth')); Point newFourthPos = textOffsetToPosition(tester, kFourLines.indexOf('Fourth'));
expect(newFirstPos.y, lessThan(firstPos.y)); expect(newFirstPos.y, lessThan(firstPos.y));
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isFalse);
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isTrue);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment