Commit 6d32b339 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Text selection handles track scrolled text fields (#10805)

Introduce CompositedTransformTarget and CompositedTransformFollower
widgets, corresponding render objects, and corresponding layers.

Adjust the way text fields work to use this.

Various changes I needed to debug the issues that came up.
parent 40db1e4b
...@@ -4,23 +4,71 @@ ...@@ -4,23 +4,71 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'print.dart';
/// A mixin that helps dump string representations of trees. /// A mixin that helps dump string representations of trees.
abstract class TreeDiagnosticsMixin { abstract class TreeDiagnosticsMixin {
// This class is intended to be used as a mixin, and should not be // This class is intended to be used as a mixin, and should not be
// extended directly. // extended directly.
factory TreeDiagnosticsMixin._() => null; factory TreeDiagnosticsMixin._() => null;
/// A brief description of this object, usually just the [runtimeType] and the
/// [hashCode].
///
/// See also:
///
/// * [toStringShallow], for a detailed description of the object.
/// * [toStringDeep], for a description of the subtree rooted at this object.
@override @override
String toString() => '$runtimeType#$hashCode'; String toString() => '$runtimeType#$hashCode';
/// Returns a one-line detailed description of the object.
///
/// This description includes everything from [debugFillDescription], but does
/// not recurse to any children.
///
/// The [toStringShallow] method can take an argument, which is the string to
/// place between each part obtained from [debugFillDescription]. Passing a
/// string such as `'\n '` will result in a multiline string that indents the
/// properties of the object below its name (as per [toString]).
///
/// See also:
///
/// * [toString], for a brief description of the object.
/// * [toStringDeep], for a description of the subtree rooted at this object.
String toStringShallow([String joiner = '; ']) {
final StringBuffer result = new StringBuffer();
result.write(toString());
result.write(joiner);
final List<String> description = <String>[];
debugFillDescription(description);
result.write(description.join(joiner));
return result.toString();
}
/// Returns a string representation of this node and its descendants. /// Returns a string representation of this node and its descendants.
///
/// This includes the information from [debugFillDescription], and then
/// recurses into the children using [debugDescribeChildren].
///
/// The [toStringDeep] method takes arguments, but those are intended for
/// internal use when recursing to the descendants, and so can be ignored.
///
/// See also:
///
/// * [toString], for a brief description of the object but not its children.
/// * [toStringShallow], for a detailed description of the object but not its
/// children.
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) { String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
String result = '$prefixLineOne$this\n'; String result = '$prefixLineOne$this\n';
final String childrenDescription = debugDescribeChildren(prefixOtherLines); final String childrenDescription = debugDescribeChildren(prefixOtherLines);
final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines '; final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines ';
final List<String> description = <String>[]; final List<String> description = <String>[];
debugFillDescription(description); debugFillDescription(description);
result += description.map((String description) => '$descriptionPrefix$description\n').join(); result += description
.expand((String description) => debugWordWrap(description, 65, wrapIndent: ' '))
.map<String>((String line) => "$descriptionPrefix$line\n")
.join();
if (childrenDescription == '') { if (childrenDescription == '') {
final String prefix = prefixOtherLines.trimRight(); final String prefix = prefixOtherLines.trimRight();
if (prefix != '') if (prefix != '')
...@@ -31,7 +79,8 @@ abstract class TreeDiagnosticsMixin { ...@@ -31,7 +79,8 @@ abstract class TreeDiagnosticsMixin {
return result; return result;
} }
/// Add additional information to the given description for use by [toStringDeep]. /// Add additional information to the given description for use by
/// [toStringDeep] and [toStringShallow].
@protected @protected
@mustCallSuper @mustCallSuper
void debugFillDescription(List<String> description) { } void debugFillDescription(List<String> description) { }
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'flat_button.dart'; import 'flat_button.dart';
...@@ -90,8 +91,17 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -90,8 +91,17 @@ class _TextSelectionToolbar extends StatelessWidget {
/// Centers the toolbar around the given position, ensuring that it remains on /// Centers the toolbar around the given position, ensuring that it remains on
/// screen. /// screen.
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
_TextSelectionToolbarLayout(this.position); _TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position);
/// The size of the screen at the time that the toolbar was last laid out.
final Size screenSize;
/// Size and position of the editing region at the time the toolbar was last
/// laid out, in global coordinates.
final Rect globalEditableRegion;
/// Anchor position of the toolbar, relative to the top left of the
/// [globalEditableRegion].
final Offset position; final Offset position;
@override @override
...@@ -101,17 +111,20 @@ class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { ...@@ -101,17 +111,20 @@ class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
@override @override
Offset getPositionForChild(Size size, Size childSize) { Offset getPositionForChild(Size size, Size childSize) {
double x = position.dx - childSize.width / 2.0; final Offset globalPosition = globalEditableRegion.topLeft + position;
double y = position.dy - childSize.height;
double x = globalPosition.dx - childSize.width / 2.0;
double y = globalPosition.dy - childSize.height;
if (x < _kToolbarScreenPadding) if (x < _kToolbarScreenPadding)
x = _kToolbarScreenPadding; x = _kToolbarScreenPadding;
else if (x + childSize.width > size.width - 2 * _kToolbarScreenPadding) else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
x = size.width - childSize.width - _kToolbarScreenPadding; x = screenSize.width - childSize.width - _kToolbarScreenPadding;
if (y < _kToolbarScreenPadding) if (y < _kToolbarScreenPadding)
y = _kToolbarScreenPadding; y = _kToolbarScreenPadding;
else if (y + childSize.height > size.height - 2 * _kToolbarScreenPadding) else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
y = size.height - childSize.height - _kToolbarScreenPadding; y = screenSize.height - childSize.height - _kToolbarScreenPadding;
return new Offset(x, y); return new Offset(x, y);
} }
...@@ -149,15 +162,17 @@ class _MaterialTextSelectionControls extends TextSelectionControls { ...@@ -149,15 +162,17 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
/// Builder for material-style copy/paste text selection toolbar. /// Builder for material-style copy/paste text selection toolbar.
@override @override
Widget buildToolbar( Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) {
BuildContext context, Offset position, TextSelectionDelegate delegate) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
final Size screenSize = MediaQuery.of(context).size;
return new ConstrainedBox( return new ConstrainedBox(
constraints: new BoxConstraints.loose(screenSize), constraints: new BoxConstraints.tight(globalEditableRegion.size),
child: new CustomSingleChildLayout( child: new CustomSingleChildLayout(
delegate: new _TextSelectionToolbarLayout(position), delegate: new _TextSelectionToolbarLayout(
child: new _TextSelectionToolbar(delegate) MediaQuery.of(context).size,
globalEditableRegion,
position,
),
child: new _TextSelectionToolbar(delegate),
) )
); );
} }
......
...@@ -1829,7 +1829,7 @@ abstract class RenderBox extends RenderObject { ...@@ -1829,7 +1829,7 @@ abstract class RenderBox extends RenderObject {
/// Subclasses that apply transforms during painting should override this /// Subclasses that apply transforms during painting should override this
/// function to factor those transforms into the calculation. /// function to factor those transforms into the calculation.
/// ///
/// The RenderBox implementation takes care of adjusting the matrix for the /// The [RenderBox] implementation takes care of adjusting the matrix for the
/// position of the given child as determined during layout and stored on the /// position of the given child as determined during layout and stored on the
/// child's [parentData] in the [BoxParentData.offset] field. /// child's [parentData] in the [BoxParentData.offset] field.
@override @override
......
...@@ -123,8 +123,13 @@ bool debugCheckIntrinsicSizes = false; ...@@ -123,8 +123,13 @@ bool debugCheckIntrinsicSizes = false;
bool debugProfilePaintsEnabled = false; bool debugProfilePaintsEnabled = false;
/// Returns a list of strings representing the given transform in a format useful for [RenderObject.debugFillDescription]. /// Returns a list of strings representing the given transform in a format
/// useful for [RenderObject.debugFillDescription].
///
/// If the argument is null, returns a list with the single string "null".
List<String> debugDescribeTransform(Matrix4 transform) { List<String> debugDescribeTransform(Matrix4 transform) {
if (transform == null)
return const <String>['null'];
final List<String> matrix = transform.toString().split('\n').map((String s) => ' $s').toList(); final List<String> matrix = transform.toString().split('\n').map((String s) => ' $s').toList();
matrix.removeLast(); matrix.removeLast();
return matrix; return matrix;
......
...@@ -30,8 +30,9 @@ typedef void SelectionChangedHandler(TextSelection selection, RenderEditable ren ...@@ -30,8 +30,9 @@ typedef void SelectionChangedHandler(TextSelection selection, RenderEditable ren
/// Used by [RenderEditable.onCaretChanged]. /// Used by [RenderEditable.onCaretChanged].
typedef void CaretChangedHandler(Rect caretRect); typedef void CaretChangedHandler(Rect caretRect);
/// Represents a global screen coordinate of the point in a selection, and the /// Represents the coordinates of the point in a selection, and the text
/// text direction at that point. /// direction at that point, relative to top left of the [RenderEditable] that
/// holds the selection.
@immutable @immutable
class TextSelectionPoint { class TextSelectionPoint {
/// Creates a description of a point in a text selection. /// Creates a description of a point in a text selection.
...@@ -40,7 +41,8 @@ class TextSelectionPoint { ...@@ -40,7 +41,8 @@ class TextSelectionPoint {
const TextSelectionPoint(this.point, this.direction) const TextSelectionPoint(this.point, this.direction)
: assert(point != null); : assert(point != null);
/// Screen coordinates of the lower left or lower right corner of the selection. /// Coordinates of the lower left or lower right corner of the selection,
/// relative to the top left of the [RenderEditable] object.
final Offset point; final Offset point;
/// Direction of the text at this edge of the selection. /// Direction of the text at this edge of the selection.
...@@ -316,7 +318,7 @@ class RenderEditable extends RenderBox { ...@@ -316,7 +318,7 @@ class RenderEditable extends RenderBox {
bool _hasVisualOverflow = false; bool _hasVisualOverflow = false;
/// Returns the global coordinates of the endpoints of the given selection. /// Returns the local 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
/// returned list is of length one. Otherwise, the selection is not collapsed /// returned list is of length one. Otherwise, the selection is not collapsed
...@@ -333,14 +335,14 @@ class RenderEditable extends RenderBox { ...@@ -333,14 +335,14 @@ class RenderEditable extends RenderBox {
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary. // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
final Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
final Offset start = new Offset(0.0, _preferredLineHeight) + caretOffset + paintOffset; final Offset start = new Offset(0.0, _preferredLineHeight) + caretOffset + paintOffset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)]; return <TextSelectionPoint>[new TextSelectionPoint(start, null)];
} else { } else {
final List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection); final List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
final Offset start = new Offset(boxes.first.start, boxes.first.bottom) + paintOffset; final Offset start = new Offset(boxes.first.start, boxes.first.bottom) + paintOffset;
final Offset end = new Offset(boxes.last.end, boxes.last.bottom) + paintOffset; final Offset end = new Offset(boxes.last.end, boxes.last.bottom) + paintOffset;
return <TextSelectionPoint>[ return <TextSelectionPoint>[
new TextSelectionPoint(localToGlobal(start), boxes.first.direction), new TextSelectionPoint(start, boxes.first.direction),
new TextSelectionPoint(localToGlobal(end), boxes.last.direction), new TextSelectionPoint(end, boxes.last.direction),
]; ];
} }
} }
......
...@@ -56,13 +56,29 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset); ...@@ -56,13 +56,29 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset);
/// child might be recorded in separate compositing layers. For this reason, do /// child might be recorded in separate compositing layers. For this reason, do
/// not hold a reference to the canvas across operations that might paint /// not hold a reference to the canvas across operations that might paint
/// child render objects. /// child render objects.
///
/// New [PaintingContext] objects are created automatically when using
/// [PaintingContext.repaintCompositedChild] and [pushLayer].
class PaintingContext { class PaintingContext {
PaintingContext._(this._containerLayer, this._paintBounds) PaintingContext._(this._containerLayer, this.canvasBounds)
: assert(_containerLayer != null), : assert(_containerLayer != null),
assert(_paintBounds != null); assert(canvasBounds != null);
final ContainerLayer _containerLayer; final ContainerLayer _containerLayer;
final Rect _paintBounds;
/// The bounds within which the painting context's [canvas] will record
/// painting commands.
///
/// A render object provided with this [PaintingContext] (e.g. in its
/// [RenderObject.paint] method) is permitted to paint outside the region that
/// the render object occupies during layout, but is not permitted to paint
/// outside these paints bounds. These paint bounds are used to construct
/// memory-efficient composited layers, which means attempting to paint
/// outside these bounds can attempt to write to pixels that do not exist in
/// the composited layer.
///
/// The [paintBounds] rectangle is in the [canvas] coordinate system.
final Rect canvasBounds;
/// Repaint the given render object. /// Repaint the given render object.
/// ///
...@@ -70,6 +86,11 @@ class PaintingContext { ...@@ -70,6 +86,11 @@ class PaintingContext {
/// composited layer, and must be in need of painting. The render object's /// composited layer, and must be in need of painting. The render object's
/// layer, if any, is re-used, along with any layers in the subtree that don't /// layer, if any, is re-used, along with any layers in the subtree that don't
/// need to be repainted. /// need to be repainted.
///
/// See also:
///
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
/// has a composited layer.
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) { static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
assert(child.isRepaintBoundary); assert(child.isRepaintBoundary);
assert(child._needsPaint); assert(child._needsPaint);
...@@ -97,7 +118,7 @@ class PaintingContext { ...@@ -97,7 +118,7 @@ class PaintingContext {
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} }
/// Paint a child render object. /// Paint a child [RenderObject].
/// ///
/// If the child has its own composited layer, the child will be composited /// If the child has its own composited layer, the child will be composited
/// into the layer subtree associated with this painting context. Otherwise, /// into the layer subtree associated with this painting context. Otherwise,
...@@ -180,6 +201,8 @@ class PaintingContext { ...@@ -180,6 +201,8 @@ class PaintingContext {
/// The current canvas can change whenever you paint a child using this /// The current canvas can change whenever you paint a child using this
/// context, which means it's fragile to hold a reference to the canvas /// context, which means it's fragile to hold a reference to the canvas
/// returned by this getter. /// returned by this getter.
///
/// Only calls within the [canvasBounds] will be recorded.
Canvas get canvas { Canvas get canvas {
if (_canvas == null) if (_canvas == null)
_startRecording(); _startRecording();
...@@ -188,9 +211,9 @@ class PaintingContext { ...@@ -188,9 +211,9 @@ class PaintingContext {
void _startRecording() { void _startRecording() {
assert(!_isRecording); assert(!_isRecording);
_currentLayer = new PictureLayer(); _currentLayer = new PictureLayer(canvasBounds);
_recorder = new ui.PictureRecorder(); _recorder = new ui.PictureRecorder();
_canvas = new Canvas(_recorder, _paintBounds); _canvas = new Canvas(_recorder, canvasBounds);
_containerLayer.append(_currentLayer); _containerLayer.append(_currentLayer);
} }
...@@ -203,14 +226,14 @@ class PaintingContext { ...@@ -203,14 +226,14 @@ class PaintingContext {
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 6.0 ..strokeWidth = 6.0
..color = debugCurrentRepaintColor.toColor(); ..color = debugCurrentRepaintColor.toColor();
canvas.drawRect(_paintBounds.deflate(3.0), paint); canvas.drawRect(canvasBounds.deflate(3.0), paint);
} }
if (debugPaintLayerBordersEnabled) { if (debugPaintLayerBordersEnabled) {
final Paint paint = new Paint() final Paint paint = new Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 1.0 ..strokeWidth = 1.0
..color = debugPaintLayerBordersColor; ..color = debugPaintLayerBordersColor;
canvas.drawRect(_paintBounds, paint); canvas.drawRect(canvasBounds, paint);
} }
return true; return true;
}); });
...@@ -262,7 +285,7 @@ class PaintingContext { ...@@ -262,7 +285,7 @@ class PaintingContext {
} }
/// Appends the given layer to the recording, and calls the `painter` callback /// Appends the given layer to the recording, and calls the `painter` callback
/// with that layer, providing the [childPaintBounds] as the paint bounds of /// with that layer, providing the `childPaintBounds` as the paint bounds of
/// the child. Canvas recording commands are not guaranteed to be stored /// the child. Canvas recording commands are not guaranteed to be stored
/// outside of the paint bounds. /// outside of the paint bounds.
/// ///
...@@ -272,9 +295,11 @@ class PaintingContext { ...@@ -272,9 +295,11 @@ class PaintingContext {
/// ///
/// The `offset` is the offset to pass to the `painter`. /// The `offset` is the offset to pass to the `painter`.
/// ///
/// If the `childPaintBounds` are not specified then the current layer's /// If the `childPaintBounds` are not specified then the current layer's paint
/// bounds are used. This is appropriate if the child layer does not apply any /// bounds are used. This is appropriate if the child layer does not apply any
/// transformation or clipping to its contents. /// transformation or clipping to its contents. The `childPaintBounds`, if
/// specified, must be in the coordinate system of the new layer, and should
/// not go outside the current layer's paint bounds.
/// ///
/// See also: /// See also:
/// ///
...@@ -285,7 +310,7 @@ class PaintingContext { ...@@ -285,7 +310,7 @@ class PaintingContext {
assert(painter != null); assert(painter != null);
_stopRecordingIfNeeded(); _stopRecordingIfNeeded();
_appendLayer(childLayer); _appendLayer(childLayer);
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? _paintBounds); final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? canvasBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext._stopRecordingIfNeeded();
} }
...@@ -379,7 +404,7 @@ class PaintingContext { ...@@ -379,7 +404,7 @@ class PaintingContext {
new TransformLayer(transform: effectiveTransform), new TransformLayer(transform: effectiveTransform),
painter, painter,
offset, offset,
childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, _paintBounds), childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, canvasBounds),
); );
} else { } else {
canvas.save(); canvas.save();
...@@ -406,6 +431,9 @@ class PaintingContext { ...@@ -406,6 +431,9 @@ class PaintingContext {
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) {
pushLayer(new OpacityLayer(alpha: alpha), painter, offset); pushLayer(new OpacityLayer(alpha: alpha), painter, offset);
} }
@override
String toString() => '$runtimeType#$hashCode(layer: $_containerLayer, canvas bounds: $canvasBounds)';
} }
/// An abstract set of layout constraints. /// An abstract set of layout constraints.
...@@ -1981,6 +2009,9 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -1981,6 +2009,9 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// frequently might want to repaint themselves without requiring their parent /// frequently might want to repaint themselves without requiring their parent
/// to repaint. /// to repaint.
/// ///
/// If this getter returns true, the [paintBounds] are applied to this object
/// and all descendants.
///
/// Warning: This getter must not change value over the lifetime of this object. /// Warning: This getter must not change value over the lifetime of this object.
bool get isRepaintBoundary => false; bool get isRepaintBoundary => false;
...@@ -2272,12 +2303,15 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -2272,12 +2303,15 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// The bounds within which this render object will paint. /// The bounds within which this render object will paint.
/// ///
/// A render object is permitted to paint outside the region it occupies /// A render object and its descendants are permitted to paint outside the
/// during layout but is not permitted to paint outside these paints bounds. /// region it occupies during layout, but they are not permitted to paint
/// These paint bounds are used to construct memory-efficient composited /// outside these paints bounds. These paint bounds are used to construct
/// layers, which means attempting to paint outside these bounds can attempt /// memory-efficient composited layers, which means attempting to paint
/// to write to pixels that do not exist in this render object's composited /// outside these bounds can attempt to write to pixels that do not exist in
/// layer. /// this render object's composited layer.
///
/// The [paintBounds] are only actually enforced when the render object is a
/// repaint boundary; see [isRepaintBoundary].
Rect get paintBounds; Rect get paintBounds;
/// Override this method to paint debugging information. /// Override this method to paint debugging information.
......
...@@ -1636,7 +1636,7 @@ class RenderTransform extends RenderProxyBox { ...@@ -1636,7 +1636,7 @@ class RenderTransform extends RenderProxyBox {
Matrix4 inverse; Matrix4 inverse;
try { try {
inverse = new Matrix4.inverted(_effectiveTransform); inverse = new Matrix4.inverted(_effectiveTransform);
} catch (e) { } on ArgumentError {
// We cannot invert the effective transform. That means the child // We cannot invert the effective transform. That means the child
// doesn't appear on screen and cannot be hit. // doesn't appear on screen and cannot be hit.
return false; return false;
...@@ -1661,7 +1661,6 @@ class RenderTransform extends RenderProxyBox { ...@@ -1661,7 +1661,6 @@ class RenderTransform extends RenderProxyBox {
@override @override
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.multiply(_effectiveTransform); transform.multiply(_effectiveTransform);
super.applyPaintTransform(child, transform);
} }
@override @override
...@@ -1785,7 +1784,7 @@ class RenderFittedBox extends RenderProxyBox { ...@@ -1785,7 +1784,7 @@ class RenderFittedBox extends RenderProxyBox {
Matrix4 inverse; Matrix4 inverse;
try { try {
inverse = new Matrix4.inverted(_transform); inverse = new Matrix4.inverted(_transform);
} catch (e) { } on ArgumentError {
// We cannot invert the effective transform. That means the child // We cannot invert the effective transform. That means the child
// doesn't appear on screen and cannot be hit. // doesn't appear on screen and cannot be hit.
return false; return false;
...@@ -1798,7 +1797,6 @@ class RenderFittedBox extends RenderProxyBox { ...@@ -1798,7 +1797,6 @@ class RenderFittedBox extends RenderProxyBox {
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
_updatePaintData(); _updatePaintData();
transform.multiply(_transform); transform.multiply(_transform);
super.applyPaintTransform(child, transform);
} }
@override @override
...@@ -1864,7 +1862,6 @@ class RenderFractionalTranslation extends RenderProxyBox { ...@@ -1864,7 +1862,6 @@ class RenderFractionalTranslation extends RenderProxyBox {
@override @override
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(translation.dx * size.width, translation.dy * size.height); transform.translate(translation.dx * size.width, translation.dy * size.height);
super.applyPaintTransform(child, transform);
} }
@override @override
...@@ -3046,3 +3043,194 @@ class RenderExcludeSemantics extends RenderProxyBox { ...@@ -3046,3 +3043,194 @@ class RenderExcludeSemantics extends RenderProxyBox {
description.add('excluding: $excluding'); description.add('excluding: $excluding');
} }
} }
/// Provides an anchor for a [RenderFollowerLayer].
///
/// See also:
///
/// * [CompositedTransformTarget], the corresponding widget.
/// * [LeaderLayer], the layer that this render object creates.
class RenderLeaderLayer extends RenderProxyBox {
/// Creates a render object that uses a [LeaderLayer].
///
/// The [link] must not be null.
RenderLeaderLayer({
@required LayerLink link,
RenderBox child,
}) : assert(link != null),
super(child) {
this.link = link;
}
/// The link object that connects this [RenderLeaderLayer] with one or more
/// [RenderFollowerLayer]s.
///
/// This property must not be null. The object must not be associated with
/// another [RenderLeaderLayer] that is also being painted.
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
assert(value != null);
if (_link == value)
return;
_link = value;
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => true;
@override
void paint(PaintingContext context, Offset offset) {
context.pushLayer(new LeaderLayer(link: link, offset: offset), super.paint, Offset.zero);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('link: $link');
}
}
/// Transform the child so that its origin is [offset] from the orign of the
/// [RenderLeaderLayer] with the same [LayerLink].
///
/// The [RenderLeaderLayer] in question must be earlier in the paint order.
///
/// Hit testing on descendants of this render object will only work if the
/// target position is within the box that this render object's parent considers
/// to be hitable.
///
/// See also:
///
/// * [CompositedTransformFollower], the corresponding widget.
/// * [FollowerLayer], the layer that this render object creates.
class RenderFollowerLayer extends RenderProxyBox {
/// Creates a render object that uses a [FollowerLayer].
///
/// The [link] and [offset] arguments must not be null.
RenderFollowerLayer({
@required LayerLink link,
bool showWhenUnlinked: true,
Offset offset: Offset.zero,
RenderBox child,
}) : assert(link != null),
assert(showWhenUnlinked != null),
assert(offset != null),
super(child) {
this.link = link;
this.showWhenUnlinked = showWhenUnlinked;
this.offset = offset;
}
/// The link object that connects this [RenderFollowerLayer] with a
/// [RenderLeaderLayer] earlier in the paint order.
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
assert(value != null);
if (_link == value)
return;
_link = value;
markNeedsPaint();
}
/// Whether to show the render object's contents when there is no
/// corresponding [RenderLeaderLayer] with the same [link].
///
/// When the render object is linked, the child is positioned such that it has
/// the same global position as the linked [RenderLeaderLayer].
///
/// When the render object is not linked, then: if [showWhenUnlinked] is true,
/// the child is visible and not repositioned; if it is false, then child is
/// hidden.
bool get showWhenUnlinked => _showWhenUnlinked;
bool _showWhenUnlinked;
set showWhenUnlinked(bool value) {
assert(value != null);
if (_showWhenUnlinked == value)
return;
_showWhenUnlinked = value;
markNeedsPaint();
}
/// The offset to apply to the origin of the linked [RenderLeaderLayer] to
/// obtain this render object's origin.
Offset get offset => _offset;
Offset _offset;
set offset(Offset value) {
assert(value != null);
if (_offset == value)
return;
_offset = value;
markNeedsPaint();
}
@override
void detach() {
_layer = null;
super.detach();
}
@override
bool get alwaysNeedsCompositing => true;
/// The layer we created when we were last painted.
FollowerLayer _layer;
Matrix4 getCurrentTransform() {
return _layer?.getLastTransform() ?? new Matrix4.identity();
}
@override
bool hitTest(HitTestResult result, { Offset position }) {
Matrix4 inverse;
try {
inverse = new Matrix4.inverted(getCurrentTransform());
} on ArgumentError {
// We cannot invert the effective transform. That means the child
// doesn't appear on screen and cannot be hit.
return false;
}
position = MatrixUtils.transformPoint(inverse, position);
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
assert(showWhenUnlinked != null);
_layer = new FollowerLayer(
link: link,
showWhenUnlinked: showWhenUnlinked,
linkedOffset: this.offset,
unlinkedOffset: offset,
);
context.pushLayer(
_layer,
super.paint,
Offset.zero,
childPaintBounds: new Rect.fromLTRB(
// We don't know where we'll end up, so we have no idea what our cull rect should be.
double.NEGATIVE_INFINITY,
double.NEGATIVE_INFINITY,
double.INFINITY,
double.INFINITY,
),
);
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.multiply(getCurrentTransform());
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('link: $link');
description.add('showWhenUnlinked: $showWhenUnlinked');
description.add('offset: $offset');
description.add('current transform matrix:');
description.addAll(debugDescribeTransform(getCurrentTransform()));
}
}
This diff is collapsed.
...@@ -263,6 +263,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -263,6 +263,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
TextSelectionOverlay _selectionOverlay; TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = new ScrollController(); final ScrollController _scrollController = new ScrollController();
final LayerLink _layerLink = new LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
// State lifecycle: // State lifecycle:
...@@ -272,6 +273,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -272,6 +273,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
super.initState(); super.initState();
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
} }
@override @override
...@@ -436,6 +438,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -436,6 +438,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
context: context, context: context,
value: _value, value: _value,
debugRequiredFor: widget, debugRequiredFor: widget,
layerLink: _layerLink,
renderObject: renderObject, renderObject: renderObject,
onSelectionOverlayChanged: _handleSelectionOverlayChanged, onSelectionOverlayChanged: _handleSelectionOverlayChanged,
selectionControls: widget.selectionControls, selectionControls: widget.selectionControls,
...@@ -538,7 +541,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -538,7 +541,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
controller: _scrollController, controller: _scrollController,
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new _Editable( return new CompositedTransformTarget(
link: _layerLink,
child: new _Editable(
value: _value, value: _value,
style: widget.style, style: widget.style,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
...@@ -551,6 +556,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -551,6 +556,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
offset: offset, offset: offset,
onSelectionChanged: _handleSelectionChanged, onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged, onCaretChanged: _handleCaretChanged,
),
); );
}, },
); );
......
...@@ -24,6 +24,10 @@ import 'scroll_position_with_single_context.dart'; ...@@ -24,6 +24,10 @@ import 'scroll_position_with_single_context.dart';
/// to an individual [Scrollable] widget. To use a custom [ScrollPosition], /// to an individual [Scrollable] widget. To use a custom [ScrollPosition],
/// subclass [ScrollController] and override [createScrollPosition]. /// subclass [ScrollController] and override [createScrollPosition].
/// ///
/// A [ScrollController] is a [Listenable]. It notifies its listeners whenever
/// any of the attached [ScrollPosition]s notify _their_ listeners (i.e.
/// whenever any of them scroll).
///
/// Typically used with [ListView], [GridView], [CustomScrollView]. /// Typically used with [ListView], [GridView], [CustomScrollView].
/// ///
/// See also: /// See also:
......
...@@ -73,8 +73,7 @@ abstract class TextSelectionControls { ...@@ -73,8 +73,7 @@ abstract class TextSelectionControls {
/// Builds a toolbar near a text selection. /// Builds a toolbar near a text selection.
/// ///
/// Typically displays buttons for copying and pasting text. /// Typically displays buttons for copying and pasting text.
// TODO(mpcomplete): A single position is probably insufficient. Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate);
Widget buildToolbar(BuildContext context, Offset position, TextSelectionDelegate delegate);
/// Returns the size of the selection handle. /// Returns the size of the selection handle.
Size get handleSize; Size get handleSize;
...@@ -92,7 +91,8 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -92,7 +91,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
@required TextEditingValue value, @required TextEditingValue value,
@required this.context, @required this.context,
this.debugRequiredFor, this.debugRequiredFor,
this.renderObject, @required this.layerLink,
@required this.renderObject,
this.onSelectionOverlayChanged, this.onSelectionOverlayChanged,
this.selectionControls, this.selectionControls,
}): assert(value != null), }): assert(value != null),
...@@ -113,6 +113,10 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -113,6 +113,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// Debugging information for explaining why the [Overlay] is required. /// Debugging information for explaining why the [Overlay] is required.
final Widget debugRequiredFor; final Widget debugRequiredFor;
/// The object supplied to the [CompositedTransformTarget] that wraps the text
/// field.
final LayerLink layerLink;
// TODO(mpcomplete): what if the renderObject is removed or replaced, or // TODO(mpcomplete): what if the renderObject is removed or replaced, or
// moves? Not sure what cases I need to handle, or how to handle them. // moves? Not sure what cases I need to handle, or how to handle them.
/// The editable line in which the selected text is being displayed. /// The editable line in which the selected text is being displayed.
...@@ -149,8 +153,8 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -149,8 +153,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
void showHandles() { void showHandles() {
assert(_handles == null); assert(_handles == null);
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.start)), new OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.end)), new OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
]; ];
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles); Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
_handleController.forward(from: 0.0); _handleController.forward(from: 0.0);
...@@ -184,6 +188,14 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -184,6 +188,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
} }
} }
/// Causes the overlay to update its rendering.
///
/// This is intended to be called when the [renderObject] may have changed its
/// text metrics (e.g. because the text was scrolled).
void updateForScroll() {
_markNeedsBuild();
}
void _markNeedsBuild([Duration duration]) { void _markNeedsBuild([Duration duration]) {
if (_handles != null) { if (_handles != null) {
_handles[0].markNeedsBuild(); _handles[0].markNeedsBuild();
...@@ -223,10 +235,11 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -223,10 +235,11 @@ class TextSelectionOverlay implements TextSelectionDelegate {
child: new _TextSelectionHandleOverlay( child: new _TextSelectionHandleOverlay(
onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); }, onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); },
onSelectionHandleTapped: _handleSelectionHandleTapped, onSelectionHandleTapped: _handleSelectionHandleTapped,
layerLink: layerLink,
renderObject: renderObject, renderObject: renderObject,
selection: _selection, selection: _selection,
selectionControls: selectionControls, selectionControls: selectionControls,
position: position position: position,
) )
); );
} }
...@@ -241,12 +254,22 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -241,12 +254,22 @@ class TextSelectionOverlay implements TextSelectionDelegate {
(endpoints.length == 1) ? (endpoints.length == 1) ?
endpoints[0].point.dx : endpoints[0].point.dx :
(endpoints[0].point.dx + endpoints[1].point.dx) / 2.0, (endpoints[0].point.dx + endpoints[1].point.dx) / 2.0,
endpoints[0].point.dy - renderObject.size.height endpoints[0].point.dy - renderObject.size.height,
);
final Rect editingRegion = new Rect.fromPoints(
renderObject.localToGlobal(Offset.zero),
renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
); );
return new FadeTransition( return new FadeTransition(
opacity: _toolbarOpacity, opacity: _toolbarOpacity,
child: selectionControls.buildToolbar(context, midpoint, this) child: new CompositedTransformFollower(
link: layerLink,
showWhenUnlinked: false,
offset: -editingRegion.topLeft,
child: selectionControls.buildToolbar(context, editingRegion, midpoint, this),
),
); );
} }
...@@ -298,16 +321,18 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -298,16 +321,18 @@ class TextSelectionOverlay implements TextSelectionDelegate {
class _TextSelectionHandleOverlay extends StatefulWidget { class _TextSelectionHandleOverlay extends StatefulWidget {
const _TextSelectionHandleOverlay({ const _TextSelectionHandleOverlay({
Key key, Key key,
this.selection, @required this.selection,
this.position, @required this.position,
this.renderObject, @required this.layerLink,
this.onSelectionHandleChanged, @required this.renderObject,
this.onSelectionHandleTapped, @required this.onSelectionHandleChanged,
this.selectionControls @required this.onSelectionHandleTapped,
@required this.selectionControls
}) : super(key: key); }) : super(key: key);
final TextSelection selection; final TextSelection selection;
final _TextSelectionHandlePosition position; final _TextSelectionHandlePosition position;
final LayerLink layerLink;
final RenderEditable renderObject; final RenderEditable renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged; final ValueChanged<TextSelection> onSelectionHandleChanged;
final VoidCallback onSelectionHandleTapped; final VoidCallback onSelectionHandleTapped;
...@@ -379,7 +404,10 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay ...@@ -379,7 +404,10 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
break; break;
} }
return new GestureDetector( return new CompositedTransformFollower(
link: widget.layerLink,
showWhenUnlinked: false,
child: new GestureDetector(
onPanStart: _handleDragStart, onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate, onPanUpdate: _handleDragUpdate,
onTap: _handleTap, onTap: _handleTap,
...@@ -388,10 +416,11 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay ...@@ -388,10 +416,11 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
new Positioned( new Positioned(
left: point.dx, left: point.dx,
top: point.dy, top: point.dy,
child: widget.selectionControls.buildHandle(context, type) child: widget.selectionControls.buildHandle(context, type),
) ),
] ],
) ),
),
); );
} }
......
...@@ -27,7 +27,7 @@ class MockClipboard { ...@@ -27,7 +27,7 @@ class MockClipboard {
Widget overlay(Widget child) { Widget overlay(Widget child) {
return new MediaQuery( return new MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(size: const Size(800.0, 600.0)),
child: new Overlay( child: new Overlay(
initialEntries: <OverlayEntry>[ initialEntries: <OverlayEntry>[
new OverlayEntry( new OverlayEntry(
...@@ -73,10 +73,22 @@ void main() { ...@@ -73,10 +73,22 @@ void main() {
return renderEditable; return renderEditable;
} }
List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
return points.map((TextSelectionPoint point) {
return new TextSelectionPoint(
box.localToGlobal(point.point),
point.direction,
);
}).toList();
}
Offset textOffsetToPosition(WidgetTester tester, int offset) { Offset textOffsetToPosition(WidgetTester tester, int offset) {
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection( final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(
new TextSelection.collapsed(offset: offset), new TextSelection.collapsed(offset: offset),
),
renderEditable,
); );
expect(endpoints.length, 1); expect(endpoints.length, 1);
return endpoints[0].point + const Offset(0.0, -2.0); return endpoints[0].point + const Offset(0.0, -2.0);
...@@ -309,15 +321,19 @@ void main() { ...@@ -309,15 +321,19 @@ void main() {
await tester.pump(const Duration(seconds: 2)); await tester.pump(const Duration(seconds: 2));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final TextSelection selection = controller.selection; final TextSelection selection = controller.selection;
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(selection); final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2); expect(endpoints.length, 2);
// Drag the right handle 2 letters to the right. // Drag the right handle 2 letters to the right.
// Note: use a small offset because the endpoint is on the very corner // We use a small offset because the endpoint is on the very corner
// of the handle. // of the handle.
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
Offset newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2); Offset newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2);
...@@ -368,10 +384,15 @@ void main() { ...@@ -368,10 +384,15 @@ void main() {
// Tap the selection handle to bring up the "paste / select all" menu. // Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
RenderEditable renderEditable = findRenderEditable(tester); RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection); List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
// SELECT ALL should select all the text. // SELECT ALL should select all the text.
await tester.tap(find.text('SELECT ALL')); await tester.tap(find.text('SELECT ALL'));
...@@ -388,10 +409,15 @@ void main() { ...@@ -388,10 +409,15 @@ void main() {
// Tap again to bring back the menu. // Tap again to bring back the menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
renderEditable = findRenderEditable(tester); renderEditable = findRenderEditable(tester);
endpoints = renderEditable.getEndpointsForSelection(controller.selection); endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
// PASTE right before the 'e'. // PASTE right before the 'e'.
await tester.tap(find.text('PASTE')); await tester.tap(find.text('PASTE'));
...@@ -422,8 +448,12 @@ void main() { ...@@ -422,8 +448,12 @@ void main() {
// Tap the selection handle to bring up the "paste / select all" menu. // Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection); final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -547,12 +577,16 @@ void main() { ...@@ -547,12 +577,16 @@ void main() {
await tester.pump(const Duration(seconds: 2)); await tester.pump(const Duration(seconds: 2));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
expect(controller.selection.baseOffset, 39); expect(controller.selection.baseOffset, 39);
expect(controller.selection.extentOffset, 44); expect(controller.selection.extentOffset, 44);
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection); final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 2); expect(endpoints.length, 2);
// Drag the right handle to the third line, just after 'Third'. // Drag the right handle to the third line, just after 'Third'.
...@@ -653,7 +687,10 @@ void main() { ...@@ -653,7 +687,10 @@ void main() {
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection); final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 2); expect(endpoints.length, 2);
// Drag the left handle to the first line, just after 'First'. // Drag the left handle to the first line, just after 'First'.
...@@ -1410,11 +1447,15 @@ void main() { ...@@ -1410,11 +1447,15 @@ void main() {
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final RenderEditable renderEditable = findRenderEditable(tester); final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(textController.selection); renderEditable.getEndpointsForSelection(textController.selection),
renderEditable,
);
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
await tester.tap(find.text('PASTE')); await tester.tap(find.text('PASTE'));
......
// Copyright 2017 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
void main() {
testWidgets('Composited transforms - only offsets', (WidgetTester tester) async {
final LayerLink link = new LayerLink();
final GlobalKey key = new GlobalKey();
await tester.pumpWidget(
new Stack(
children: <Widget>[
new Positioned(
left: 123.0,
top: 456.0,
child: new CompositedTransformTarget(
link: link,
child: new Container(height: 10.0, width: 10.0),
),
),
new Positioned(
left: 787.0,
top: 343.0,
child: new CompositedTransformFollower(
link: link,
child: new Container(key: key, height: 10.0, width: 10.0),
),
),
],
),
);
final RenderBox box = key.currentContext.findRenderObject();
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
});
testWidgets('Composited transforms - with rotations', (WidgetTester tester) async {
final LayerLink link = new LayerLink();
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
await tester.pumpWidget(
new Stack(
children: <Widget>[
new Positioned(
top: 123.0,
left: 456.0,
child: new Transform.rotate(
angle: 1.0, // radians
child: new CompositedTransformTarget(
link: link,
child: new Container(key: key1, height: 10.0, width: 10.0),
),
),
),
new Positioned(
top: 787.0,
left: 343.0,
child: new Transform.rotate(
angle: -0.3, // radians
child: new CompositedTransformFollower(
link: link,
child: new Container(key: key2, height: 10.0, width: 10.0),
),
),
),
],
),
);
final RenderBox box1 = key1.currentContext.findRenderObject();
final RenderBox box2 = key2.currentContext.findRenderObject();
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx));
expect(position1.dy, moreOrLessEquals(position2.dy));
});
testWidgets('Composited transforms - nested', (WidgetTester tester) async {
final LayerLink link = new LayerLink();
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
await tester.pumpWidget(
new Stack(
children: <Widget>[
new Positioned(
top: 123.0,
left: 456.0,
child: new Transform.rotate(
angle: 1.0, // radians
child: new CompositedTransformTarget(
link: link,
child: new Container(key: key1, height: 10.0, width: 10.0),
),
),
),
new Positioned(
top: 787.0,
left: 343.0,
child: new Transform.rotate(
angle: -0.3, // radians
child: new Padding(
padding: const EdgeInsets.all(20.0),
child: new CompositedTransformFollower(
link: new LayerLink(),
child: new Transform(
transform: new Matrix4.skew(0.9, 1.1),
child: new Padding(
padding: const EdgeInsets.all(20.0),
child: new CompositedTransformFollower(
link: link,
child: new Container(key: key2, height: 10.0, width: 10.0),
),
),
),
),
),
),
),
],
),
);
final RenderBox box1 = key1.currentContext.findRenderObject();
final RenderBox box2 = key2.currentContext.findRenderObject();
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx));
expect(position1.dy, moreOrLessEquals(position2.dy));
});
testWidgets('Composited transforms - hit testing', (WidgetTester tester) async {
final LayerLink link = new LayerLink();
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
final GlobalKey key3 = new GlobalKey();
bool _tapped = false;
await tester.pumpWidget(
new Stack(
children: <Widget>[
new Positioned(
left: 123.0,
top: 456.0,
child: new CompositedTransformTarget(
link: link,
child: new Container(key: key1, height: 10.0, width: 10.0),
),
),
new CompositedTransformFollower(
link: link,
child: new GestureDetector(
key: key2,
behavior: HitTestBehavior.opaque,
onTap: () { _tapped = true; },
child: new Container(key: key3, height: 10.0, width: 10.0),
),
),
],
),
);
final RenderBox box2 = key2.currentContext.findRenderObject();
expect(box2.size, const Size(10.0, 10.0));
expect(_tapped, isFalse);
await tester.tap(find.byKey(key1));
expect(_tapped, 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