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),
]; ];
} }
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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:collection';
import 'dart:ui' as ui show ImageFilter, Picture, SceneBuilder; import 'dart:ui' as ui show ImageFilter, Picture, SceneBuilder;
import 'dart:ui' show Offset; import 'dart:ui' show Offset;
...@@ -90,7 +91,7 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin { ...@@ -90,7 +91,7 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin {
/// Override this method to upload this layer to the engine. /// Override this method to upload this layer to the engine.
/// ///
/// The layerOffset is the accumulated offset of this layer's parent from the /// The `layerOffset` is the accumulated offset of this layer's parent from the
/// origin of the builder's coordinate system. /// origin of the builder's coordinate system.
void addToScene(ui.SceneBuilder builder, Offset layerOffset); void addToScene(ui.SceneBuilder builder, Offset layerOffset);
...@@ -117,6 +118,16 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin { ...@@ -117,6 +118,16 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin {
/// ///
/// Picture layers are always leaves in the layer tree. /// Picture layers are always leaves in the layer tree.
class PictureLayer extends Layer { class PictureLayer extends Layer {
PictureLayer(this.canvasBounds);
/// The bounds that were used for the canvas that drew this layer's [picture].
///
/// This is purely advisory. It is included in the information dumped with
/// [dumpLayerTree] (which can be triggered by pressing "L" when using
/// "flutter run" at the console), which can help debug why certain drawing
/// commands are being culled.
final Rect canvasBounds;
/// The picture recorded for this layer. /// The picture recorded for this layer.
/// ///
/// The picture's coodinate system matches this layer's coodinate system. /// The picture's coodinate system matches this layer's coodinate system.
...@@ -150,6 +161,12 @@ class PictureLayer extends Layer { ...@@ -150,6 +161,12 @@ class PictureLayer extends Layer {
void addToScene(ui.SceneBuilder builder, Offset layerOffset) { void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint); builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
} }
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('paint bounds: $canvasBounds');
}
} }
/// A layer that indicates to the compositor that it should display /// A layer that indicates to the compositor that it should display
...@@ -357,6 +374,44 @@ class ContainerLayer extends Layer { ...@@ -357,6 +374,44 @@ class ContainerLayer extends Layer {
} }
} }
/// Applies the transform that would be applied when compositing the given
/// child to the given matrix.
///
/// Specifically, this should apply the transform that is applied to child's
/// _origin_. When using [applyTransform] with a chain of layers, results will
/// be unreliable unless the deepest layer in the chain collapses the
/// `layerOffset` in [addToScene] to zero, meaning that it passes
/// [Offset.zero] to its children, and bakes any incoming `layerOffset` into
/// the [SceneBuilder] as (for instance) a transform (which is then also
/// included in the transformation applied by [applyTransform]).
///
/// For example, if [addToScene] applies the `layerOffset` and then
/// passes [Offset.zero] to the children, then it should be included in the
/// transform applied here, whereas if [addToScene] just passes the
/// `layerOffset` to the child, then it should not be included in the
/// transform applied here.
///
/// This method is only valid immediately after [addToScene] has been called,
/// before any of the properties have been changed.
///
/// The default implementation does nothing, since [ContainerLayer], by
/// default, composits its children at the origin of the [ContainerLayer]
/// itself.
///
/// The `child` argument should generally not be null, since in principle a
/// layer could transform each child independently. However, certain layers
/// may explicitly allow null as a value, for example if they know that they
/// transform all their children identically.
///
/// The `transform` argument must not be null.
///
/// Used by [FollowerLayer] to transform its child to a [LeaderLayer]'s
/// position.
void applyTransform(Layer child, Matrix4 transform) {
assert(child != null);
assert(transform != null);
}
@override @override
String debugDescribeChildren(String prefix) { String debugDescribeChildren(String prefix) {
if (firstChild == null) if (firstChild == null)
...@@ -391,13 +446,17 @@ class ContainerLayer extends Layer { ...@@ -391,13 +446,17 @@ class ContainerLayer extends Layer {
class OffsetLayer extends ContainerLayer { class OffsetLayer extends ContainerLayer {
/// Creates an offset layer. /// Creates an offset layer.
/// ///
/// By default, [offset] is zero. /// By default, [offset] is zero. It must be non-null before the compositing
/// phase of the pipeline.
OffsetLayer({ this.offset: Offset.zero }); OffsetLayer({ this.offset: Offset.zero });
/// Offset from parent in the parent's coordinate system. /// Offset from parent in the parent's coordinate system.
/// ///
/// The scene must be explicitly recomposited after this property is changed /// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]). /// (as described at [Layer]).
///
/// The [offset] property must be non-null before the compositing phase of the
/// pipeline.
Offset offset; Offset offset;
@override @override
...@@ -412,7 +471,6 @@ class OffsetLayer extends ContainerLayer { ...@@ -412,7 +471,6 @@ class OffsetLayer extends ContainerLayer {
} }
} }
/// A composite layer that clips its children using a rectangle. /// A composite layer that clips its children using a rectangle.
class ClipRectLayer extends ContainerLayer { class ClipRectLayer extends ContainerLayer {
/// Creates a layer with a rectangular clip. /// Creates a layer with a rectangular clip.
...@@ -497,35 +555,51 @@ class ClipPathLayer extends ContainerLayer { ...@@ -497,35 +555,51 @@ class ClipPathLayer extends ContainerLayer {
} }
} }
/// A composited layer that applies a transformation matrix to its children. /// A composited layer that applies a given transformation matrix to its
/// children.
///
/// This class inherits from [OffsetLayer] to make it one of the layers that
/// can be used at the root of a [RenderObject] hierarchy.
class TransformLayer extends OffsetLayer { class TransformLayer extends OffsetLayer {
/// Creates a transform layer. /// Creates a transform layer.
/// ///
/// The [transform] property must be non-null before the compositing phase of /// The [transform] and [offset] properties must be non-null before the
/// the pipeline. /// compositing phase of the pipeline.
TransformLayer({ TransformLayer({ this.transform, Offset offset: Offset.zero }) : super(offset: offset);
this.transform
});
/// The matrix to apply. /// The matrix to apply.
/// ///
/// The scene must be explicitly recomposited after this property is changed /// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]). /// (as described at [Layer]).
///
/// This transform is applied before [offset], if both are set.
///
/// The [transform] property must be non-null before the compositing phase of
/// the pipeline.
Matrix4 transform; Matrix4 transform;
Matrix4 _lastEffectiveTransform;
@override @override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) { void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
assert(offset == Offset.zero); _lastEffectiveTransform = transform;
Matrix4 effectiveTransform = transform; final Offset totalOffset = offset + layerOffset;
if (layerOffset != Offset.zero) { if (totalOffset != Offset.zero) {
effectiveTransform = new Matrix4.translationValues(layerOffset.dx, layerOffset.dy, 0.0) _lastEffectiveTransform = new Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0)
..multiply(transform); ..multiply(_lastEffectiveTransform);
} }
builder.pushTransform(effectiveTransform.storage); builder.pushTransform(_lastEffectiveTransform.storage);
addChildrenToScene(builder, Offset.zero); addChildrenToScene(builder, Offset.zero);
builder.pop(); builder.pop();
} }
@override
void applyTransform(Layer child, Matrix4 transform) {
assert(child != null);
assert(transform != null);
transform.multiply(_lastEffectiveTransform);
}
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
...@@ -565,7 +639,7 @@ class OpacityLayer extends ContainerLayer { ...@@ -565,7 +639,7 @@ class OpacityLayer extends ContainerLayer {
} }
} }
/// A composited layer that applies a shader to hits children. /// A composited layer that applies a shader to its children.
class ShaderMaskLayer extends ContainerLayer { class ShaderMaskLayer extends ContainerLayer {
/// Creates a shader mask layer. /// Creates a shader mask layer.
/// ///
...@@ -682,3 +756,303 @@ class PhysicalModelLayer extends ContainerLayer { ...@@ -682,3 +756,303 @@ class PhysicalModelLayer extends ContainerLayer {
description.add('clipRRect: $clipRRect'); description.add('clipRRect: $clipRRect');
} }
} }
/// An object that a [LeaderLayer] can register with.
///
/// An instance of this class should be provided as the [LeaderLayer.link] and
/// the [FollowerLayer.link] properties to cause the [FollowerLayer] to follow
/// the [LeaderLayer].
///
/// See also:
///
/// * [CompositedTransformTarget], the widget that creates a [LeaderLayer].
/// * [CompositedTransformFollower], the widget that creates a [FollowerLayer].
/// * [RenderLeaderLayer] and [RenderFollowerLayer], the corresponding
/// render objects.
class LayerLink {
/// The currently-registered [LeaderLayer], if any.
LeaderLayer get leader => _leader;
LeaderLayer _leader;
@override
String toString() => '$runtimeType#$hashCode(${ _leader != null ? "<linked>" : "<dangling>" })';
}
/// A composited layer that can be followed by a [FollowerLayer].
///
/// This layer collapses the accumulated offset into a transform and passes
/// [Offset.zero] to its child layers in the [addToScene]/[addChildrenToScene]
/// methods, so that [applyTransform] will work reliably.
class LeaderLayer extends ContainerLayer {
/// Creates a leader layer.
///
/// The [link] property must not be null, and must not have been provided to
/// any other [LeaderLayer] layers that are [attached] to the layer tree at
/// the same time.
///
/// The [offset] property must be non-null before the compositing phase of the
/// pipeline.
LeaderLayer({ @required this.link, this.offset: Offset.zero }) : assert(link != null);
/// The object with which this layer should register.
///
/// The link will be established when this layer is [attach]ed, and will be
/// cleared when this layer is [detach]ed.
final LayerLink link;
/// Offset from parent in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
///
/// The [offset] property must be non-null before the compositing phase of the
/// pipeline.
Offset offset;
@override
void attach(Object owner) {
super.attach(owner);
assert(link.leader == null);
_lastOffset = null;
link._leader = this;
}
@override
void detach() {
assert(link.leader == this);
link._leader = null;
_lastOffset = null;
super.detach();
}
/// The offset the last time this layer was composited.
///
/// This is reset to null when the layer is attached or detached, to help
/// catch cases where the follower layer ends up before the leader layer, but
/// not every case can be detected.
Offset _lastOffset;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
assert(offset != null);
_lastOffset = offset + layerOffset;
if (_lastOffset != Offset.zero)
builder.pushTransform(new Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage);
addChildrenToScene(builder, Offset.zero);
if (_lastOffset != Offset.zero)
builder.pop();
}
/// Applies the transform that would be applied when compositing the given
/// child to the given matrix.
///
/// See [ContainerLayer.applyTransform] for details.
///
/// The `child` argument may be null, as the same transform is applied to all
/// children.
@override
void applyTransform(Layer child, Matrix4 transform) {
assert(_lastOffset != null);
if (_lastOffset != Offset.zero)
transform.translate(_lastOffset.dx, _lastOffset.dy);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('offset: $offset');
description.add('link: $link');
}
}
/// A composited layer that applies a transformation matrix to its children such
/// that they are positioned to match a [LeaderLayer].
///
/// If any of the ancestors of this layer have a degenerate matrix (e.g. scaling
/// by zero), then the [FollowerLayer] will not be able to transform its child
/// to the coordinate space of the [Leader].
///
/// A [linkedOffset] property can be provided to further offset the child layer
/// from the leader layer, for example if the child is to follow the linked
/// layer at a distance rather than directly overlapping it.
class FollowerLayer extends ContainerLayer {
/// Creates a follower layer.
///
/// The [link] property must not be null.
///
/// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties
/// must be non-null before the compositing phase of the pipeline.
FollowerLayer({
@required this.link,
this.showWhenUnlinked: true,
this.unlinkedOffset: Offset.zero,
this.linkedOffset: Offset.zero,
}) : assert(link != null);
/// The link to the [LeaderLayer].
///
/// The same object should be provided to a [LeaderLayer] that is earlier in
/// the layer tree. When this layer is composited, it will apply a transform
/// that moves its children to match the position of the [LeaderLayer].
final LayerLink link;
/// Whether to show the layer's contents when the [link] does not point to a
/// [LeaderLayer].
///
/// When the layer is linked, children layers are positioned such that they
/// have the same global position as the linked [LeaderLayer].
///
/// When the layer is not linked, then: if [showWhenUnlinked] is true,
/// children are positioned as if the [FollowerLayer] was a [ContainerLayer];
/// if it is false, then children are hidden.
///
/// The [showWhenUnlinked] property must be non-null before the compositing
/// phase of the pipeline.
bool showWhenUnlinked;
/// Offset from parent in the parent's coordinate system, used when the layer
/// is not linked to a [LeaderLayer].
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
///
/// The [unlinkedOffset] property must be non-null before the compositing
/// phase of the pipeline.
///
/// See also:
///
/// * [linkedOffset], for when the layers are linked.
Offset unlinkedOffset;
/// Offset from the origin of the leader layer to the origin of the child
/// layers, used when the layer is linked to a [LeaderLayer].
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
///
/// The [linkedOffset] property must be non-null before the compositing phase
/// of the pipeline.
///
/// See also:
///
/// * [unlinkedOffset], for when the layer is not linked.
Offset linkedOffset;
Offset _lastOffset;
Matrix4 _lastTransform;
/// The transform that was used during the last composition phase.
///
/// If the [link] was not linked to a [LeaderLayer], or if this layer has
/// a degerenate matrix applied, then this will be null.
///
/// This method returns a new [Matrix4] instance each time it is invoked.
Matrix4 getLastTransform() {
if (_lastTransform == null)
return null;
final Matrix4 result = new Matrix4.translationValues(-_lastOffset.dx, -_lastOffset.dy, 0.0);
result.multiply(_lastTransform);
return result;
}
/// Call [applyTransform] for each layer in the provided list.
///
/// The list is in reverse order (deepest first). The first layer will be
/// treated as the child of the second, and so forth. The first layer in the
/// list won't have [applyTransform] called on it. The first layer may be
/// null.
Matrix4 _collectTransformForLayerChain(List<ContainerLayer> layers) {
// Initialize our result matrix.
final Matrix4 result = new Matrix4.identity();
// Apply each layer to the matrix in turn, starting from the last layer,
// and providing the previous layer as the child.
for (int index = layers.length - 1; index > 0; index -= 1)
layers[index].applyTransform(layers[index - 1], result);
return result;
}
/// Populate [_lastTransform] given the current state of the tree.
void _establishTransform() {
assert(link != null);
_lastTransform = null;
// Check to see if we are linked.
if (link.leader == null)
return;
// If we're linked, check the link is valid.
assert(link.leader.owner == owner, 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.');
assert(link.leader._lastOffset != null, 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.');
// Collect all our ancestors into a Set so we can recognize them.
final Set<Layer> ancestors = new HashSet<Layer>();
Layer ancestor = parent;
while (ancestor != null) {
ancestors.add(ancestor);
ancestor = ancestor.parent;
}
// Collect all the layers from a hypothetical child (null) of the target
// layer up to the common ancestor layer.
ContainerLayer layer = link.leader;
final List<ContainerLayer> forwardLayers = <ContainerLayer>[null, layer];
do {
layer = layer.parent;
forwardLayers.add(layer);
} while (!ancestors.contains(layer));
ancestor = layer;
// Collect all the layers from this layer up to the common ancestor layer.
layer = this;
final List<ContainerLayer> inverseLayers = <ContainerLayer>[layer];
do {
layer = layer.parent;
inverseLayers.add(layer);
} while (layer != ancestor);
// Establish the forward and backward matrices given these lists of layers.
final Matrix4 forwardTransform = _collectTransformForLayerChain(forwardLayers);
final Matrix4 inverseTransform = _collectTransformForLayerChain(inverseLayers);
if (inverseTransform.invert() == 0.0) {
// We are in a degenerate transform, so there's not much we can do.
return;
}
// Combine the matrices and store the result.
inverseTransform.multiply(forwardTransform);
inverseTransform.translate(linkedOffset.dx, linkedOffset.dy);
_lastTransform = inverseTransform;
}
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
assert(link != null);
assert(showWhenUnlinked != null);
if (link.leader == null && !showWhenUnlinked) {
_lastTransform = null;
_lastOffset = null;
return;
}
_establishTransform();
if (_lastTransform != null) {
builder.pushTransform(_lastTransform.storage);
addChildrenToScene(builder, Offset.zero);
builder.pop();
_lastOffset = unlinkedOffset + layerOffset;
} else {
_lastOffset = null;
addChildrenToScene(builder, unlinkedOffset + layerOffset);
}
}
@override
void applyTransform(Layer child, Matrix4 transform) {
assert(child != null);
assert(transform != null);
if (_lastTransform != null)
transform.multiply(_lastTransform);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('link: $link');
if (_lastTransform != null) {
description.add('transform:');
description.addAll(debugDescribeTransform(getLastTransform()));
}
}
}
...@@ -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()));
}
}
...@@ -26,6 +26,7 @@ export 'package:flutter/rendering.dart' show ...@@ -26,6 +26,7 @@ export 'package:flutter/rendering.dart' show
FlowPaintingContext, FlowPaintingContext,
FractionalOffsetTween, FractionalOffsetTween,
HitTestBehavior, HitTestBehavior,
LayerLink,
MainAxisAlignment, MainAxisAlignment,
MainAxisSize, MainAxisSize,
MultiChildLayoutDelegate, MultiChildLayoutDelegate,
...@@ -301,11 +302,13 @@ class CustomPaint extends SingleChildRenderObjectWidget { ...@@ -301,11 +302,13 @@ class CustomPaint extends SingleChildRenderObjectWidget {
final Size size; final Size size;
@override @override
RenderCustomPaint createRenderObject(BuildContext context) => new RenderCustomPaint( RenderCustomPaint createRenderObject(BuildContext context) {
painter: painter, return new RenderCustomPaint(
foregroundPainter: foregroundPainter, painter: painter,
preferredSize: size, foregroundPainter: foregroundPainter,
); preferredSize: size,
);
}
@override @override
void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) { void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) {
...@@ -711,12 +714,14 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -711,12 +714,14 @@ class Transform extends SingleChildRenderObjectWidget {
final bool transformHitTests; final bool transformHitTests;
@override @override
RenderTransform createRenderObject(BuildContext context) => new RenderTransform( RenderTransform createRenderObject(BuildContext context) {
transform: transform, return new RenderTransform(
origin: origin, transform: transform,
alignment: alignment, origin: origin,
transformHitTests: transformHitTests alignment: alignment,
); transformHitTests: transformHitTests
);
}
@override @override
void updateRenderObject(BuildContext context, RenderTransform renderObject) { void updateRenderObject(BuildContext context, RenderTransform renderObject) {
...@@ -728,6 +733,140 @@ class Transform extends SingleChildRenderObjectWidget { ...@@ -728,6 +733,140 @@ class Transform extends SingleChildRenderObjectWidget {
} }
} }
/// A widget that can be targetted by a [CompositedTransformFollower].
///
/// When this widget is composited during the compositing phase (which comes
/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it
/// updates the [link] object so that any [CompositedTransformFollower] widgets
/// that are subsequently composited in the same frame and were given the same
/// [LayerLink] can position themselves at the same screen location.
///
/// A single [CompositedTransformTarget] can be followed by multiple
/// [CompositedTransformFollower] widgets.
///
/// The [CompositedTransformTarget] must come earlier in the paint order than
/// any linked [CompositedTransformFollower]s.
///
/// See also:
///
/// * [CompositedTransformFollower], the widget that can target this one.
/// * [LeaderLayer], the layer that implements this widget's logic.
class CompositedTransformTarget extends SingleChildRenderObjectWidget {
/// Creates a composited transform target widget.
///
/// The [link] property must not be null, and must not be currently being used
/// by any other [CompositedTransformTarget] object that is in the tree.
const CompositedTransformTarget({
Key key,
@required this.link,
Widget child,
}) : assert(link != null),
super(key: key, child: child);
/// The link object that connects this [CompositedTransformTarget] with one or
/// more [CompositedTransformFollower]s.
///
/// This property must not be null. The object must not be associated with
/// another [CompositedTransformTarget] that is also being painted.
final LayerLink link;
@override
RenderLeaderLayer createRenderObject(BuildContext context) {
return new RenderLeaderLayer(
link: link,
);
}
@override
void updateRenderObject(BuildContext context, RenderLeaderLayer renderObject) {
renderObject
..link = link;
}
}
/// A widget that follows a [CompositedTransformTarget].
///
/// When this widget is composited during the compositing phase (which comes
/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it
/// applies a transformation that causes it to provide its child with a
/// coordinate space that matches that of the linked [CompositedTransformTarget]
/// widget, offset by [offset].
///
/// The [LayerLink] object used as the [link] must be the same object as that
/// provided to the matching [CompositedTransformTarget].
///
/// The [CompositedTransformTarget] must come earlier in the paint order than
/// this [CompositedTransformFollower].
///
/// Hit testing on descendants of this widget will only work if the target
/// position is within the box that this widget's parent considers to be
/// hitable. If the parent covers the screen, this is trivially achievable, so
/// this widget is usually used as the root of an [OverlayEntry] in an app-wide
/// [Overlay] (e.g. as created by the [MaterialApp] widget's [Navigator]).
///
/// See also:
///
/// * [CompositedTransformTarget], the widget that this widget can target.
/// * [FollowerLayer], the layer that implements this widget's logic.
/// * [Transform], which applies an arbitrary transform to a child.
class CompositedTransformFollower extends SingleChildRenderObjectWidget {
/// Creates a composited transform target widget.
///
/// The [link] property must not be null. If it was also provided to a
/// [CompositedTransformTarget], that widget must come earlier in the paint
/// order.
///
/// The [showWhenUnlinked] and [offset] properties must also not be null.
const CompositedTransformFollower({
Key key,
@required this.link,
this.showWhenUnlinked: true,
this.offset: Offset.zero,
Widget child,
}) : assert(link != null),
assert(showWhenUnlinked != null),
assert(offset != null),
super(key: key, child: child);
/// The link object that connects this [CompositedTransformFollower] with a
/// [CompositedTransformTarget].
///
/// This property must not be null.
final LayerLink link;
/// Whether to show the widget's contents when there is no corresponding
/// [CompositedTransformTarget] with the same [link].
///
/// When the widget is linked, the child is positioned such that it has the
/// same global position as the linked [CompositedTransformTarget].
///
/// When the widget is not linked, then: if [showWhenUnlinked] is true, the
/// child is visible and not repositioned; if it is false, then child is
/// hidden.
final bool showWhenUnlinked;
/// The offset to apply to the origin of the linked
/// [CompositedTransformTarget] to obtain this widget's origin.
final Offset offset;
@override
RenderFollowerLayer createRenderObject(BuildContext context) {
return new RenderFollowerLayer(
link: link,
showWhenUnlinked: showWhenUnlinked,
offset: offset,
);
}
@override
void updateRenderObject(BuildContext context, RenderFollowerLayer renderObject) {
renderObject
..link = link
..showWhenUnlinked = showWhenUnlinked
..offset = offset;
}
}
/// Scales and positions its child within itself according to [fit]. /// Scales and positions its child within itself according to [fit].
/// ///
/// See also: /// See also:
...@@ -1207,9 +1346,11 @@ class SizedBox extends SingleChildRenderObjectWidget { ...@@ -1207,9 +1346,11 @@ class SizedBox extends SingleChildRenderObjectWidget {
final double height; final double height;
@override @override
RenderConstrainedBox createRenderObject(BuildContext context) => new RenderConstrainedBox( RenderConstrainedBox createRenderObject(BuildContext context) {
additionalConstraints: _additionalConstraints, return new RenderConstrainedBox(
); additionalConstraints: _additionalConstraints,
);
}
BoxConstraints get _additionalConstraints { BoxConstraints get _additionalConstraints {
return new BoxConstraints.tightFor(width: width, height: height); return new BoxConstraints.tightFor(width: width, height: height);
...@@ -1353,11 +1494,13 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget { ...@@ -1353,11 +1494,13 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
final FractionalOffset alignment; final FractionalOffset alignment;
@override @override
RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) => new RenderFractionallySizedOverflowBox( RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) {
alignment: alignment, return new RenderFractionallySizedOverflowBox(
widthFactor: widthFactor, alignment: alignment,
heightFactor: heightFactor widthFactor: widthFactor,
); heightFactor: heightFactor
);
}
@override @override
void updateRenderObject(BuildContext context, RenderFractionallySizedOverflowBox renderObject) { void updateRenderObject(BuildContext context, RenderFractionallySizedOverflowBox renderObject) {
...@@ -1423,10 +1566,12 @@ class LimitedBox extends SingleChildRenderObjectWidget { ...@@ -1423,10 +1566,12 @@ class LimitedBox extends SingleChildRenderObjectWidget {
final double maxHeight; final double maxHeight;
@override @override
RenderLimitedBox createRenderObject(BuildContext context) => new RenderLimitedBox( RenderLimitedBox createRenderObject(BuildContext context) {
maxWidth: maxWidth, return new RenderLimitedBox(
maxHeight: maxHeight maxWidth: maxWidth,
); maxHeight: maxHeight
);
}
@override @override
void updateRenderObject(BuildContext context, RenderLimitedBox renderObject) { void updateRenderObject(BuildContext context, RenderLimitedBox renderObject) {
...@@ -1489,13 +1634,15 @@ class OverflowBox extends SingleChildRenderObjectWidget { ...@@ -1489,13 +1634,15 @@ class OverflowBox extends SingleChildRenderObjectWidget {
final double maxHeight; final double maxHeight;
@override @override
RenderConstrainedOverflowBox createRenderObject(BuildContext context) => new RenderConstrainedOverflowBox( RenderConstrainedOverflowBox createRenderObject(BuildContext context) {
alignment: alignment, return new RenderConstrainedOverflowBox(
minWidth: minWidth, alignment: alignment,
maxWidth: maxWidth, minWidth: minWidth,
minHeight: minHeight, maxWidth: maxWidth,
maxHeight: maxHeight minHeight: minHeight,
); maxHeight: maxHeight
);
}
@override @override
void updateRenderObject(BuildContext context, RenderConstrainedOverflowBox renderObject) { void updateRenderObject(BuildContext context, RenderConstrainedOverflowBox renderObject) {
...@@ -3196,18 +3343,20 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -3196,18 +3343,20 @@ class RawImage extends LeafRenderObjectWidget {
final Rect centerSlice; final Rect centerSlice;
@override @override
RenderImage createRenderObject(BuildContext context) => new RenderImage( RenderImage createRenderObject(BuildContext context) {
image: image, return new RenderImage(
width: width, image: image,
height: height, width: width,
scale: scale, height: height,
color: color, scale: scale,
colorBlendMode: colorBlendMode, color: color,
fit: fit, colorBlendMode: colorBlendMode,
alignment: alignment, fit: fit,
repeat: repeat, alignment: alignment,
centerSlice: centerSlice repeat: repeat,
); centerSlice: centerSlice
);
}
@override @override
void updateRenderObject(BuildContext context, RenderImage renderObject) { void updateRenderObject(BuildContext context, RenderImage renderObject) {
...@@ -3371,13 +3520,15 @@ class Listener extends SingleChildRenderObjectWidget { ...@@ -3371,13 +3520,15 @@ class Listener extends SingleChildRenderObjectWidget {
final HitTestBehavior behavior; final HitTestBehavior behavior;
@override @override
RenderPointerListener createRenderObject(BuildContext context) => new RenderPointerListener( RenderPointerListener createRenderObject(BuildContext context) {
onPointerDown: onPointerDown, return new RenderPointerListener(
onPointerMove: onPointerMove, onPointerDown: onPointerDown,
onPointerUp: onPointerUp, onPointerMove: onPointerMove,
onPointerCancel: onPointerCancel, onPointerUp: onPointerUp,
behavior: behavior onPointerCancel: onPointerCancel,
); behavior: behavior
);
}
@override @override
void updateRenderObject(BuildContext context, RenderPointerListener renderObject) { void updateRenderObject(BuildContext context, RenderPointerListener renderObject) {
...@@ -3499,10 +3650,12 @@ class IgnorePointer extends SingleChildRenderObjectWidget { ...@@ -3499,10 +3650,12 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
final bool ignoringSemantics; final bool ignoringSemantics;
@override @override
RenderIgnorePointer createRenderObject(BuildContext context) => new RenderIgnorePointer( RenderIgnorePointer createRenderObject(BuildContext context) {
ignoring: ignoring, return new RenderIgnorePointer(
ignoringSemantics: ignoringSemantics ignoring: ignoring,
); ignoringSemantics: ignoringSemantics
);
}
@override @override
void updateRenderObject(BuildContext context, RenderIgnorePointer renderObject) { void updateRenderObject(BuildContext context, RenderIgnorePointer renderObject) {
...@@ -3583,10 +3736,12 @@ class MetaData extends SingleChildRenderObjectWidget { ...@@ -3583,10 +3736,12 @@ class MetaData extends SingleChildRenderObjectWidget {
final HitTestBehavior behavior; final HitTestBehavior behavior;
@override @override
RenderMetaData createRenderObject(BuildContext context) => new RenderMetaData( RenderMetaData createRenderObject(BuildContext context) {
metaData: metaData, return new RenderMetaData(
behavior: behavior metaData: metaData,
); behavior: behavior
);
}
@override @override
void updateRenderObject(BuildContext context, RenderMetaData renderObject) { void updateRenderObject(BuildContext context, RenderMetaData renderObject) {
...@@ -3668,12 +3823,14 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -3668,12 +3823,14 @@ class Semantics extends SingleChildRenderObjectWidget {
final String label; final String label;
@override @override
RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations( RenderSemanticsAnnotations createRenderObject(BuildContext context) {
container: container, return new RenderSemanticsAnnotations(
checked: checked, container: container,
selected: selected, checked: checked,
label: label, selected: selected,
); label: label,
);
}
@override @override
void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) { void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) {
......
...@@ -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,19 +541,22 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -538,19 +541,22 @@ 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(
value: _value, link: _layerLink,
style: widget.style, child: new _Editable(
cursorColor: widget.cursorColor, value: _value,
showCursor: _showCursor, style: widget.style,
maxLines: widget.maxLines, cursorColor: widget.cursorColor,
selectionColor: widget.selectionColor, showCursor: _showCursor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, maxLines: widget.maxLines,
textAlign: widget.textAlign, selectionColor: widget.selectionColor,
obscureText: widget.obscureText, textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
offset: offset, textAlign: widget.textAlign,
onSelectionChanged: _handleSelectionChanged, obscureText: widget.obscureText,
onCaretChanged: _handleCaretChanged, offset: offset,
onSelectionChanged: _handleSelectionChanged,
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,19 +404,23 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay ...@@ -379,19 +404,23 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
break; break;
} }
return new GestureDetector( return new CompositedTransformFollower(
onPanStart: _handleDragStart, link: widget.layerLink,
onPanUpdate: _handleDragUpdate, showWhenUnlinked: false,
onTap: _handleTap, child: new GestureDetector(
child: new Stack( onPanStart: _handleDragStart,
children: <Widget>[ onPanUpdate: _handleDragUpdate,
new Positioned( onTap: _handleTap,
left: point.dx, child: new Stack(
top: point.dy, children: <Widget>[
child: widget.selectionControls.buildHandle(context, type) new Positioned(
) left: point.dx,
] top: point.dy,
) 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(
new TextSelection.collapsed(offset: offset), renderEditable.getEndpointsForSelection(
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