Commit d39eb84a authored by Matt Perry's avatar Matt Perry Committed by GitHub

Add a maxLines parameter for multiline Input. (#6328)

If maxLines is 1, it's a single line Input that scrolls horizontally.
Otherwise, overflowed text wraps and scrolls vertically, taking up at
most `maxLines`.

Also fixed scrolling behavior so that the Input scrolls ensuring the
cursor is always visible.

Fixes https://github.com/flutter/flutter/issues/6271
parent 97dbd9ea
...@@ -184,7 +184,7 @@ class RenderEditable extends RenderBox { ...@@ -184,7 +184,7 @@ class RenderEditable extends RenderBox {
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); Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
Point start = new Point(0.0, constraints.constrainHeight(_preferredLineHeight)) + caretOffset + offset; Point start = new Point(0.0, _preferredLineHeight) + caretOffset + offset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)]; return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
} else { } else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection); List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
...@@ -206,10 +206,9 @@ class RenderEditable extends RenderBox { ...@@ -206,10 +206,9 @@ class RenderEditable extends RenderBox {
/// 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) {
double lineHeight = constraints.constrainHeight(_preferredLineHeight);
Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype); 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, lineHeight).shift(caretOffset + _paintOffset); return new Rect.fromLTWH(0.0, 0.0, _kCaretWidth, _preferredLineHeight).shift(caretOffset + _paintOffset);
} }
Size _contentSize; Size _contentSize;
...@@ -223,7 +222,7 @@ class RenderEditable extends RenderBox { ...@@ -223,7 +222,7 @@ class RenderEditable extends RenderBox {
// TODO(abarth): ParagraphBuilder#build's argument should be optional. // TODO(abarth): ParagraphBuilder#build's argument should be optional.
// TODO(abarth): These min/max values should be the default for ui.Paragraph. // TODO(abarth): These min/max values should be the default for ui.Paragraph.
_layoutTemplate = builder.build(new ui.ParagraphStyle()) _layoutTemplate = builder.build(new ui.ParagraphStyle())
..layout(new ui.ParagraphConstraints(width: _maxContentWidth)); ..layout(new ui.ParagraphConstraints(width: double.INFINITY));
} }
return _layoutTemplate.height; return _layoutTemplate.height;
} }
...@@ -241,7 +240,7 @@ class RenderEditable extends RenderBox { ...@@ -241,7 +240,7 @@ class RenderEditable extends RenderBox {
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
return _preferredLineHeight; return _preferredLineHeight * maxLines;
} }
@override @override
...@@ -303,12 +302,11 @@ class RenderEditable extends RenderBox { ...@@ -303,12 +302,11 @@ class RenderEditable extends RenderBox {
@override @override
void performLayout() { void performLayout() {
Size oldSize = hasSize ? size : null; Size oldSize = hasSize ? size : null;
double lineHeight = constraints.constrainHeight(_preferredLineHeight); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, _preferredLineHeight - 2.0 * _kCaretHeightOffset);
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, lineHeight - 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(lineHeight, lineHeight * _maxLines) _textPainter.height.clamp(_preferredLineHeight, _preferredLineHeight * _maxLines)
)); ));
Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
assert(_selection != null); assert(_selection != null);
......
...@@ -513,6 +513,11 @@ class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerPr ...@@ -513,6 +513,11 @@ class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerPr
/// If there are no in-progress scrolling physics, this function scrolls to /// If there are no in-progress scrolling physics, this function scrolls to
/// the given offset instead. /// the given offset instead.
void didUpdateScrollBehavior(double newScrollOffset) { void didUpdateScrollBehavior(double newScrollOffset) {
_setStateMaybeDuringBuild(() {
_contentExtent = scrollBehavior.contentExtent;
_containerExtent = scrollBehavior.containerExtent;
});
// This does not call setState, because if anything below actually // This does not call setState, because if anything below actually
// changes our build, it will itself independently trigger a frame. // changes our build, it will itself independently trigger a frame.
assert(_controller.isAnimating || _simulation == null); assert(_controller.isAnimating || _simulation == null);
...@@ -536,8 +541,6 @@ class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerPr ...@@ -536,8 +541,6 @@ class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerPr
/// [didUpdateScrollBehavior]. /// [didUpdateScrollBehavior].
/// 3. Updating this object's gesture detector with [updateGestureDetector]. /// 3. Updating this object's gesture detector with [updateGestureDetector].
void handleExtentsChanged(double contentExtent, double containerExtent) { void handleExtentsChanged(double contentExtent, double containerExtent) {
_contentExtent = contentExtent;
_containerExtent = containerExtent;
didUpdateScrollBehavior(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: contentExtent, contentExtent: contentExtent,
containerExtent: containerExtent, containerExtent: containerExtent,
......
...@@ -311,7 +311,7 @@ void main() { ...@@ -311,7 +311,7 @@ void main() {
await gesture.moveTo(newHandlePos); await gesture.moveTo(newHandlePos);
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, selection.baseOffset); expect(inputValue.selection.baseOffset, selection.baseOffset);
expect(inputValue.selection.extentOffset, selection.extentOffset+2); expect(inputValue.selection.extentOffset, selection.extentOffset+2);
...@@ -565,7 +565,7 @@ void main() { ...@@ -565,7 +565,7 @@ void main() {
await gesture.moveTo(newHandlePos); await gesture.moveTo(newHandlePos);
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pumpWidget(builder());
expect(inputValue.selection.baseOffset, 76); expect(inputValue.selection.baseOffset, 76);
expect(inputValue.selection.extentOffset, 108); expect(inputValue.selection.extentOffset, 108);
...@@ -635,7 +635,11 @@ void main() { ...@@ -635,7 +635,11 @@ void main() {
TestGesture gesture = await tester.startGesture(firstPos, pointer: 7); TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
await tester.pump(); await tester.pump();
await gesture.moveBy(new Offset(0.0, -1000.0)); await gesture.moveBy(new Offset(0.0, -1000.0));
await tester.pump(); await tester.pump(const Duration(seconds: 2));
// Wait and drag again to trigger https://github.com/flutter/flutter/issues/6329
// (No idea why this is necessary, but the bug wouldn't repro without it.)
await gesture.moveBy(new Offset(0.0, -1000.0));
await tester.pump(const Duration(seconds: 2));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
...@@ -679,5 +683,4 @@ void main() { ...@@ -679,5 +683,4 @@ void main() {
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
}); });
} }
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