diff --git a/packages/flutter/lib/src/painting/text_editing.dart b/packages/flutter/lib/src/painting/text_editing.dart index 9fde58c06f5..24403987654 100644 --- a/packages/flutter/lib/src/painting/text_editing.dart +++ b/packages/flutter/lib/src/painting/text_editing.dart @@ -6,6 +6,8 @@ import 'dart:ui' show hashValues, TextAffinity, TextPosition; export 'dart:ui' show TextAffinity, TextPosition; +import 'package:meta/meta.dart'; + /// A range of characters in a string of text. class TextRange { /// Creates a text range. @@ -15,7 +17,10 @@ class TextRange { /// /// Instead of creating an empty text range, consider using the [empty] /// constant. - const TextRange({ this.start, this.end }); + const TextRange({ + @required this.start, + @required this.end + }); /// A text range that starts and ends at offset. const TextRange.collapsed(int offset) @@ -89,8 +94,8 @@ class TextSelection extends TextRange { /// /// The [baseOffset] and [extentOffset] arguments must not be null. const TextSelection({ - int baseOffset, - int extentOffset, + @required int baseOffset, + @required int extentOffset, this.affinity: TextAffinity.downstream, this.isDirectional: false }) : baseOffset = baseOffset, @@ -108,7 +113,7 @@ class TextSelection extends TextRange { /// /// The [offset] argument must not be null. const TextSelection.collapsed({ - int offset, + @required int offset, this.affinity: TextAffinity.downstream }) : baseOffset = offset, extentOffset = offset, isDirectional = false, super.collapsed(offset); diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 1922f562c92..3c30e891e09 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -115,6 +115,10 @@ class RenderParagraph extends RenderBox { _textPainter.layout(minWidth: minWidth, maxWidth: wrap ? maxWidth : double.INFINITY); } + void _layoutTextWithConstraints(BoxConstraints constraints) { + _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + } + @override double computeMinIntrinsicWidth(double height) { _layoutText(); @@ -147,7 +151,7 @@ class RenderParagraph extends RenderBox { assert(!needsLayout); assert(constraints != null); assert(constraints.debugAssertIsValid()); - _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + _layoutTextWithConstraints(constraints); return _textPainter.computeDistanceToActualBaseline(baseline); } @@ -159,7 +163,7 @@ class RenderParagraph extends RenderBox { assert(debugHandleEvent(event, entry)); if (event is! PointerDownEvent) return; - _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + _layoutTextWithConstraints(constraints); Offset offset = entry.localPosition.toOffset(); TextPosition position = _textPainter.getPositionForOffset(offset); TextSpan span = _textPainter.text.getSpanForPosition(position); @@ -171,7 +175,7 @@ class RenderParagraph extends RenderBox { @override void performLayout() { - _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + _layoutTextWithConstraints(constraints); // We grab _textPainter.size here because assigning to `size` will trigger // us to validate our intrinsic sizes, which will change _textPainter's // layout because the intrinsic size calculations are destructive. @@ -222,7 +226,7 @@ class RenderParagraph extends RenderBox { // // If you remove this call, make sure that changing the textAlign still // works properly. - _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + _layoutTextWithConstraints(constraints); final Canvas canvas = context.canvas; if (_hasVisualOverflow) { final Rect bounds = offset & size; @@ -245,6 +249,52 @@ class RenderParagraph extends RenderBox { } } + /// Returns the offset at which to paint the caret. + /// + /// Valid only after [layout]. + Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { + assert(!needsLayout); + _layoutTextWithConstraints(constraints); + return _textPainter.getOffsetForCaret(position, caretPrototype); + } + + /// 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 getBoxesForSelection(TextSelection selection) { + assert(!needsLayout); + _layoutTextWithConstraints(constraints); + return _textPainter.getBoxesForSelection(selection); + } + + /// Returns the position within the text for the given pixel offset. + /// + /// Valid only after [layout]. + TextPosition getPositionForOffset(Offset offset) { + assert(!needsLayout); + _layoutTextWithConstraints(constraints); + return _textPainter.getPositionForOffset(offset); + } + + /// 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 + /// . + /// + /// Valid only after [layout]. + TextRange getWordBoundary(TextPosition position) { + assert(!needsLayout); + _layoutTextWithConstraints(constraints); + return _textPainter.getWordBoundary(position); + } + @override SemanticsAnnotator get semanticsAnnotator => _annotate; diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart new file mode 100644 index 00000000000..1f5f28f9c0b --- /dev/null +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -0,0 +1,75 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui show TextBox; + +import 'package:flutter/rendering.dart'; +import 'package:test/test.dart'; + +import 'rendering_tester.dart'; + +const String _kText = 'I polished up that handle so carefullee\nThat now I am the Ruler of the Queen\'s Navee!'; + +void main() { + test('getOffsetForCaret control test', () { + RenderParagraph paragraph = new RenderParagraph(new TextSpan(text: _kText)); + layout(paragraph); + + Rect caret = new Rect.fromLTWH(0.0, 0.0, 2.0, 20.0); + + Offset offset5 = paragraph.getOffsetForCaret(new TextPosition(offset: 5), caret); + expect(offset5.dx, greaterThan(0.0)); + + Offset offset25 = paragraph.getOffsetForCaret(new TextPosition(offset: 25), caret); + expect(offset25.dx, greaterThan(offset5.dx)); + + Offset offset50 = paragraph.getOffsetForCaret(new TextPosition(offset: 50), caret); + expect(offset50.dy, greaterThan(offset5.dy)); + }); + + test('getPositionForOffset control test', () { + RenderParagraph paragraph = new RenderParagraph(new TextSpan(text: _kText)); + layout(paragraph); + + TextPosition position20 = paragraph.getPositionForOffset(new Offset(20.0, 5.0)); + expect(position20.offset, greaterThan(0.0)); + + TextPosition position40 = paragraph.getPositionForOffset(new Offset(40.0, 5.0)); + expect(position40.offset, greaterThan(position20.offset)); + + TextPosition positionBelow = paragraph.getPositionForOffset(new Offset(5.0, 20.0)); + expect(positionBelow.offset, greaterThan(position40.offset)); + }); + + test('getBoxesForSelection control test', () { + RenderParagraph paragraph = new RenderParagraph(new TextSpan(text: _kText)); + layout(paragraph); + + List boxes = paragraph.getBoxesForSelection( + new TextSelection(baseOffset: 5, extentOffset: 25) + ); + + expect(boxes.length, equals(1)); + + boxes = paragraph.getBoxesForSelection( + new TextSelection(baseOffset: 25, extentOffset: 50) + ); + + expect(boxes.length, equals(3)); + }); + + test('getWordBoundary control test', () { + RenderParagraph paragraph = new RenderParagraph(new TextSpan(text: _kText)); + layout(paragraph); + + TextRange range5 = paragraph.getWordBoundary(new TextPosition(offset: 5)); + expect(range5.textInside(_kText), equals('polished')); + + TextRange range50 = paragraph.getWordBoundary(new TextPosition(offset: 50)); + expect(range50.textInside(_kText), equals(' ')); + + TextRange range85 = paragraph.getWordBoundary(new TextPosition(offset: 75)); + expect(range85.textInside(_kText), equals('Queen\'s')); + }); +}