Unverified Commit af129b61 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

`OverlayPortal.overlayChild` contributes semantics to `OverlayPortal` instead...

`OverlayPortal.overlayChild` contributes semantics to `OverlayPortal` instead of `Overlay` (#134921)

Fixes https://github.com/flutter/flutter/issues/134456
parent 6dc5d2fd
......@@ -901,17 +901,20 @@ class _TooltipOverlay extends StatelessWidget {
constraints: BoxConstraints(minHeight: height),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Container(
decoration: decoration,
padding: padding,
margin: margin,
child: Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: Text.rich(
richMessage,
style: textStyle,
textAlign: textAlign,
child: Semantics(
container: true,
child: Container(
decoration: decoration,
padding: padding,
margin: margin,
child: Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: Text.rich(
richMessage,
style: textStyle,
textAlign: textAlign,
),
),
),
),
......
......@@ -3531,7 +3531,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
// node, thus marking this semantics boundary dirty is not enough, it needs
// to find the first parent semantics boundary that does not have any
// possible sibling node.
while (node.parent is RenderObject && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
while (node.parent != null && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
if (node != this && node._needsSemanticsUpdate) {
break;
}
......@@ -3565,7 +3565,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
if (!node._needsSemanticsUpdate) {
node._needsSemanticsUpdate = true;
if (owner != null) {
assert(node._semanticsConfiguration.isSemanticBoundary || node.parent is! RenderObject);
assert(node._semanticsConfiguration.isSemanticBoundary || node.parent == null);
owner!._nodesNeedingSemantics.add(node);
owner!.requestVisualUpdate();
}
......@@ -3574,7 +3574,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
/// Updates the semantic information of the render object.
void _updateSemantics() {
assert(_semanticsConfiguration.isSemanticBoundary || parent is! RenderObject);
assert(_semanticsConfiguration.isSemanticBoundary || parent == null);
if (_needsLayout) {
// There's not enough information in this subtree to compute semantics.
// The subtree is probably being kept alive by a viewport but not laid out.
......@@ -3625,8 +3625,8 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
final bool blockChildInteractions = blockUserActions || config.isBlockingUserActions;
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
final bool hasChildConfigurationsDelegate = config.childConfigurationsDelegate != null;
final bool explicitChildNode = config.explicitChildNodes || parent == null;
final ChildSemanticsConfigurationsDelegate? childConfigurationsDelegate = config.childConfigurationsDelegate;
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
......@@ -3650,7 +3650,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
if (hasTags) {
fragment.addTags(config.tagsForChildren!);
}
if (hasChildConfigurationsDelegate && fragment.config != null) {
if (childConfigurationsDelegate != null && fragment.config != null) {
// This fragment need to go through delegate to determine whether it
// merge up or not.
childConfigurations.add(fragment.config!);
......@@ -3674,14 +3674,14 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
}
});
assert(hasChildConfigurationsDelegate || configToFragment.isEmpty);
assert(childConfigurationsDelegate != null || configToFragment.isEmpty);
if (explicitChildNode) {
for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
fragment.markAsExplicit();
}
} else if (hasChildConfigurationsDelegate) {
final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
} else if (childConfigurationsDelegate != null) {
final ChildSemanticsConfigurationsResult result = childConfigurationsDelegate(childConfigurations);
mergeUpFragments.addAll(
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
final _InterestingSemanticsFragment? fragment = configToFragment[config];
......@@ -3706,7 +3706,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
_needsSemanticsUpdate = false;
final _SemanticsFragment result;
if (parent is! RenderObject) {
if (parent == null) {
assert(!config.hasBeenAnnotated);
assert(!mergeIntoParent);
assert(siblingMergeFragmentGroups.isEmpty);
......@@ -4781,26 +4781,14 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
parentPaintClipRect: parentPaintClipRect,
)!;
final Rect fragmentRect = MatrixUtils.transformRect(geometry.transform, geometry.rect);
if (rect == null) {
rect = fragmentRect;
} else {
rect = rect.expandToInclude(fragmentRect);
}
rect = rect?.expandToInclude(fragmentRect) ?? fragmentRect;
if (geometry.semanticsClipRect != null) {
final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.semanticsClipRect!);
if (semanticsClipRect == null) {
semanticsClipRect = rect;
} else {
semanticsClipRect = semanticsClipRect.intersect(rect);
}
semanticsClipRect = semanticsClipRect?.intersect(rect) ?? rect;
}
if (geometry.paintClipRect != null) {
final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.paintClipRect!);
if (paintClipRect == null) {
paintClipRect = rect;
} else {
paintClipRect = paintClipRect.intersect(rect);
}
paintClipRect = paintClipRect?.intersect(rect) ?? rect;
}
if (switchableFragment._tagsForChildren != null) {
tags.addAll(switchableFragment._tagsForChildren!);
......@@ -4891,8 +4879,7 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
return; // Drop the node, it's not going to be visible.
}
owner._semantics ??= SemanticsNode(showOnScreen: owner.showOnScreen);
final SemanticsNode node = owner._semantics!
final SemanticsNode node = (owner._semantics ??= SemanticsNode(showOnScreen: owner.showOnScreen))
..isMergedIntoParent = _mergeIntoParent
..tags = _tagsForChildren;
......@@ -5070,24 +5057,45 @@ class _SemanticsGeometry {
_transform = Matrix4.identity();
_semanticsClipRect = parentSemanticsClipRect;
_paintClipRect = parentPaintClipRect;
for (int index = ancestors.length-1; index > 0; index -= 1) {
final RenderObject parent = ancestors[index];
final RenderObject child = ancestors[index-1];
final Rect? parentSemanticsClipRect = parent.describeSemanticsClip(child);
if (parentSemanticsClipRect != null) {
_semanticsClipRect = parentSemanticsClipRect;
_paintClipRect = _intersectRects(_paintClipRect, parent.describeApproximatePaintClip(child));
final RenderObject semanticsParent = ancestors[index];
final RenderObject semanticsChild = ancestors[index-1];
_applyIntermediatePaintTransforms(semanticsParent, semanticsChild, _transform);
if (identical(semanticsParent, semanticsChild.parent)) {
// The easier and more common case: semanticsParent is directly
// responsible for painting (and potentially clipping) the semanticsChild
// RenderObject.
_computeClipRect(semanticsParent, semanticsChild, _semanticsClipRect, _paintClipRect);
} else {
_semanticsClipRect = _intersectRects(_semanticsClipRect, parent.describeApproximatePaintClip(child));
// Otherwise we have to find the closest ancestor RenderObject that
// has up-to-date semantics geometry and compute the clip rects from there.
//
// Currently it can only happen when the subtree contains an OverlayPortal.
final List<RenderObject> clipPath = <RenderObject>[semanticsChild];
RenderObject? ancestor = semanticsChild.parent;
while (ancestor != null && ancestor._semantics == null) {
clipPath.add(ancestor);
ancestor = ancestor.parent;
}
_paintClipRect = ancestor?._semantics?.parentPaintClipRect;
_semanticsClipRect = ancestor?._semantics?.parentSemanticsClipRect;
if (ancestor != null) {
assert(ancestor._semantics != null);
assert(!ancestor._needsSemanticsUpdate);
RenderObject parent = ancestor;
for (int i = clipPath.length - 1; i >= 0; i -= 1) {
_computeClipRect(parent, clipPath[i], _semanticsClipRect, _paintClipRect);
parent = clipPath[i];
}
}
}
_temporaryTransformHolder.setIdentity(); // clears data from previous call(s)
_applyIntermediatePaintTransforms(parent, child, _transform, _temporaryTransformHolder);
_semanticsClipRect = _transformRect(_semanticsClipRect, _temporaryTransformHolder);
_paintClipRect = _transformRect(_paintClipRect, _temporaryTransformHolder);
}
final RenderObject owner = ancestors.first;
_rect = _semanticsClipRect == null ? owner.semanticBounds : _semanticsClipRect!.intersect(owner.semanticBounds);
_rect = _semanticsClipRect?.intersect(owner.semanticBounds) ?? owner.semanticBounds;
if (_paintClipRect != null) {
final Rect paintRect = _paintClipRect!.intersect(_rect);
_markAsHidden = paintRect.isEmpty && !_rect.isEmpty;
......@@ -5097,15 +5105,6 @@ class _SemanticsGeometry {
}
}
// A matrix used to store transient transform data.
//
// Reusing this matrix avoids allocating a new matrix every time a temporary
// matrix is needed.
//
// This instance should never be returned to the caller. Otherwise, the data
// stored in it will be overwritten unpredictably by subsequent reuses.
static final Matrix4 _temporaryTransformHolder = Matrix4.zero();
/// From parent to child coordinate system.
static Rect? _transformRect(Rect? rect, Matrix4 transform) {
if (rect == null) {
......@@ -5117,36 +5116,88 @@ class _SemanticsGeometry {
return MatrixUtils.inverseTransformRect(transform, rect);
}
// Calls applyPaintTransform on all of the render objects between [child] and
// [ancestor]. This method handles cases where the immediate semantic parent
// is not the immediate render object parent of the child.
// Computes the paint transform from `childFragmentOwner` to
// `parentFragmentOwner` and applies the paint transform to `transform` in
// place.
//
// It will mutate both transform and clipRectTransform.
// The `parentFragmentOwner` and `childFragmentOwner` [RenderObject]s must be
// in the same render tree (so they have a common ancestor).
static void _applyIntermediatePaintTransforms(
RenderObject ancestor,
RenderObject child,
RenderObject parentFragmentOwner,
RenderObject childFragmentOwner,
Matrix4 transform,
Matrix4 clipRectTransform,
) {
assert(clipRectTransform.isIdentity());
RenderObject intermediateParent = child.parent!;
while (intermediateParent != ancestor) {
intermediateParent.applyPaintTransform(child, transform);
intermediateParent = intermediateParent.parent!;
child = child.parent!;
Matrix4? parentToCommonAncestorTransform;
RenderObject from = childFragmentOwner;
RenderObject to = parentFragmentOwner;
while (!identical(from, to)) {
final int fromDepth = from.depth;
final int toDepth = to.depth;
if (fromDepth >= toDepth) {
assert(from.parent != null, '$parentFragmentOwner and $childFragmentOwner are not in the same render tree.');
final RenderObject fromParent = from.parent!;
fromParent.applyPaintTransform(from, transform);
from = fromParent;
}
if (fromDepth <= toDepth) {
assert(to.parent != null, '$parentFragmentOwner and $childFragmentOwner are not in the same render tree.');
final RenderObject toParent = to.parent!;
toParent.applyPaintTransform(to, parentToCommonAncestorTransform ??= Matrix4.identity());
to = toParent;
}
}
if (parentToCommonAncestorTransform != null) {
if (parentToCommonAncestorTransform.invert() != 0) {
transform.multiply(parentToCommonAncestorTransform);
} else {
transform.setZero();
}
}
ancestor.applyPaintTransform(child, transform);
ancestor.applyPaintTransform(child, clipRectTransform);
}
static Rect? _intersectRects(Rect? a, Rect? b) {
if (a == null) {
return b;
// A matrix used to store transient transform data.
//
// Reusing this matrix avoids allocating a new matrix every time a temporary
// matrix is needed.
//
// This instance should never be returned to the caller. Otherwise, the data
// stored in it will be overwritten unpredictably by subsequent reuses.
static final Matrix4 _temporaryTransformHolder = Matrix4.zero();
// Computes the semantics and painting clip rects for the given child and
// assigns the rects to _semanticsClipRect and _paintClipRect respectively.
//
// The caller must guarantee that child.parent == parent. The resulting rects
// are in `child`'s coordinate system.
void _computeClipRect(RenderObject parent, RenderObject child, Rect? parentSemanticsClipRect, Rect? parentPaintClipRect) {
assert(identical(child.parent, parent));
// Computes the paint transform from child to parent. The _transformRect
// method will compute the inverse.
_temporaryTransformHolder.setIdentity(); // clears data from previous call(s)
parent.applyPaintTransform(child, _temporaryTransformHolder);
final Rect? additionalPaintClip = parent.describeApproximatePaintClip(child);
_paintClipRect = _transformRect(
_intersectRects(additionalPaintClip, parentPaintClipRect),
_temporaryTransformHolder,
);
if (_paintClipRect == null) {
_semanticsClipRect = null;
} else {
final Rect? semanticsClip = parent.describeSemanticsClip(child) ?? _intersectRects(parentSemanticsClipRect, additionalPaintClip);
_semanticsClipRect = _transformRect(semanticsClip, _temporaryTransformHolder);
}
}
static Rect? _intersectRects(Rect? a, Rect? b) {
if (b == null) {
return a;
}
return a.intersect(b);
return a?.intersect(b) ?? b;
}
/// Whether the [SemanticsNode] annotated with the geometric information tracked
......
......@@ -923,12 +923,26 @@ class _TheaterParentData extends StackParentData {
// children that are created by an OverlayPortal.
OverlayEntry? overlayEntry;
/// A [OverlayPortal] makes its overlay child a render child of an ancestor
/// [Overlay]. Currently, to make sure the overlay child is painted after its
/// [OverlayPortal], and before the next [OverlayEntry] (which could be
/// something that should obstruct the overlay child, such as a [ModalRoute])
/// in the host [Overlay], the paint order of each overlay child is managed by
/// the [OverlayEntry] that hosts its [OverlayPortal].
///
/// The following methods are exposed to allow easy access to the overlay
/// children's render objects whose order is managed by [overlayEntry], in the
/// right order.
// _overlayStateMounted is set to null in _OverlayEntryWidgetState's dispose
// method. This property is only accessed during layout, paint and hit-test so
// the `value!` should be safe.
Iterator<RenderBox>? get paintOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.iterator;
Iterator<RenderBox>? get hitTestOrderIterator => overlayEntry?._overlayEntryStateNotifier?.value!._hitTestOrderIterable.iterator;
void visitChildrenOfOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.forEach(visitor);
// A convenience method for traversing `paintOrderIterator` with a
// [RenderObjectVisitor].
void visitOverlayPortalChildrenOnOverlayEntry(RenderObjectVisitor visitor) => overlayEntry?._overlayEntryStateNotifier?.value!._paintOrderIterable.forEach(visitor);
}
class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData>, _RenderTheaterMixin {
......@@ -978,7 +992,7 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
RenderBox? child = firstChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(_detachChild);
childParentData.visitOverlayPortalChildrenOnOverlayEntry(_detachChild);
child = childParentData.nextSibling;
}
}
......@@ -1197,7 +1211,7 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
while (child != null) {
visitor(child);
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(visitor);
childParentData.visitOverlayPortalChildrenOnOverlayEntry(visitor);
child = childParentData.nextSibling;
}
}
......@@ -1208,7 +1222,6 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
while (child != null) {
visitor(child);
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitChildrenOfOverlayEntry(visitor);
child = childParentData.nextSibling;
}
}
......@@ -1264,7 +1277,7 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
}
int subcount = 1;
childParentData.visitChildrenOfOverlayEntry((RenderObject renderObject) {
childParentData.visitOverlayPortalChildrenOnOverlayEntry((RenderObject renderObject) {
final RenderBox child = renderObject as RenderBox;
if (onstage) {
onstageChildren.add(
......@@ -1468,6 +1481,17 @@ class OverlayPortalController {
/// [OverlayPortalController.show] was called. The last [OverlayPortal] to have
/// called `show` gets to paint its overlay child in the foreground.
///
/// ### Semantics
///
/// The semantics subtree generated by the overlay child is considered attached
/// to [OverlayPortal] instead of the target [Overlay]. An [OverlayPortal]'s
/// semantics subtree can be dropped from the semantics tree due to invisibility
/// while the overlay child is still visible (for example, when the
/// [OverlayPortal] is completely invisible in a [ListView] but kept alive by
/// a [KeepAlive] widget). When this happens the semantics subtree generated by
/// the overlay child is also dropped, even if the overlay child is still visible
/// on screen.
///
/// {@template flutter.widgets.overlayPortalVsOverlayEntry}
/// ### Differences between [OverlayPortal] and [OverlayEntry]
///
......@@ -2028,8 +2052,9 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
}
}
// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` is
// laid out.
// A `RenderProxyBox` that defers its layout until its `_layoutSurrogate` (which
// is not necessarily an ancestor of this RenderBox, but shares at least one
// `_RenderTheater` ancestor with this RenderBox) is laid out.
//
// This `RenderObject` must be a child of a `_RenderTheater`. It guarantees that:
//
......@@ -2200,4 +2225,13 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
// walk.
_deferredLayoutChild?.layoutByLayoutSurrogate();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
super.visitChildrenForSemantics(visitor);
final _RenderDeferredLayoutBox? deferredChild = _deferredLayoutChild;
if (deferredChild != null) {
visitor(deferredChild);
}
}
}
......@@ -3374,29 +3374,29 @@ void main() {
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
),
],
),
],
),
TestSemantics(
id: 6,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0),
children: <TestSemantics>[
TestSemantics(
id: 7,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(
id: 8,
label: 'Item 0',
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
id: 6,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0),
children: <TestSemantics>[
TestSemantics(
id: 7,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(
id: 8,
label: 'Item 0',
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
),
],
),
],
actions: <SemanticsAction>[SemanticsAction.tap],
),
],
),
......
......@@ -1480,6 +1480,68 @@ void main() {
semantics.dispose();
});
testWidgets('Tooltip semantics does not merge into child', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return ListView(
children: <Widget>[
const Text('before'),
Tooltip(
key: tooltipKey,
showDuration: const Duration(seconds: 50),
message: 'B',
child: const Text('child'),
),
const Text('after'),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
// Starts the animation.
await tester.pump();
// Make sure the fade in animation has started and the tooltip isn't transparent.
await tester.pump(const Duration(seconds: 2));
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(label: 'before'),
TestSemantics(label: 'child', tooltip: 'B', children: <TestSemantics>[TestSemantics(label: 'B')]),
TestSemantics(label: 'after'),
],
),
],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgetsWithLeakTracking('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
Widget buildApp(String text, { required double textScaleFactor }) {
return Theme(
......
......@@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'semantics_tester.dart';
class _ManyRelayoutBoundaries extends StatelessWidget {
const _ManyRelayoutBoundaries({
required this.levels,
......@@ -269,43 +272,44 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgetsWithLeakTracking('No relayout boundary between OverlayPortal and Overlay', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/133545.
late final OverlayEntry overlayEntry;
addTearDown(() => overlayEntry..remove()..dispose());
final GlobalKey key = GlobalKey(debugLabel: 'key');
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
// The Positioned widget prevents a relayout boundary from being
// introduced between the Overlay and OverlayPortal.
return Positioned(
top: 0,
left: 0,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => SizedBox(key: key),
child: const SizedBox(),
),
);
},
),
],
),
);
testWidgetsWithLeakTracking(
'Overlay child remains accessible via tree walk when there is no relayout boundary between OverlayPortal and Overlay',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/133545.
late final OverlayEntry overlayEntry;
addTearDown(() => overlayEntry..remove()..dispose());
final GlobalKey key = GlobalKey(debugLabel: 'key');
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
overlayEntry = OverlayEntry(
builder: (BuildContext context) {
// The Positioned widget prevents a relayout boundary from being
// introduced between the Overlay and OverlayPortal.
return Positioned(
top: 0,
left: 0,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => SizedBox(key: key),
child: const SizedBox(),
),
);
},
),
],
),
);
controller1.hide();
await tester.pumpWidget(widget);
controller1.hide();
await tester.pumpWidget(widget);
controller1.show();
await tester.pump();
expect(find.byKey(key), findsOneWidget);
expect(tester.takeException(), isNull);
verifyTreeIsClean();
controller1.show();
await tester.pump();
expect(find.byKey(key), findsOneWidget);
expect(tester.takeException(), isNull);
verifyTreeIsClean();
});
testWidgets('Throws when the same controller is attached to multiple OverlayPortal', (WidgetTester tester) async {
......@@ -1975,6 +1979,255 @@ void main() {
expect(childrenVisited, containsAllInOrder(<RenderObject>[child1Box.parent!, child2Box.parent!]));
});
});
group('Semantics', () {
testWidgets('ordering and transform', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final double rowOriginY = TestSemantics.fullScreen.height - 10;
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(fontSize: 10),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Semantics(
container: true,
explicitChildNodes: true,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const Positioned(left: 0.0, top: 0.0, child: Text('BBBB')),
child: const Text('A'),
),
),
const Text('CC'),
],
),
);
},
),
],
),
);
await tester.pumpWidget(widget);
final Matrix4 node1Transform = Matrix4.identity()
..scale(3.0, 3.0, 1.0)
..translate(0.0, TestSemantics.fullScreen.height - 10.0);
final Matrix4 node4Transform = node1Transform.clone()..translate(10.0);
final TestSemantics expected = TestSemantics.root(children: <TestSemantics>[
TestSemantics(
id: 1,
rect: Offset.zero & const Size(10, 10),
transform: node1Transform,
children: <TestSemantics>[
TestSemantics(id: 2, label: 'A', rect: Offset.zero & const Size(10, 10)),
// The crossAxisAlignment is set to `end`. The size of node 1 is 30 x 10.
TestSemantics(
id: 3,
label: 'BBBB',
rect: Offset.zero & const Size(40, 10),
transform: Matrix4.translationValues(0, -rowOriginY, 0),
),
],
),
TestSemantics(
id: 4,
label: 'CC',
rect: Offset.zero & const Size(20, 10),
transform: node4Transform
),
]);
expect(semantics, hasSemantics(expected));
semantics.dispose();
});
testWidgets('OverlayPortal overlay child clipping', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(fontSize: 10),
child: ListView(
children: <Widget>[
// Clips OverlayPortal, making it only half visible.
SizedBox(height: TestSemantics.fullScreen.height - 5),
Semantics(
container: true,
explicitChildNodes: true,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return Positioned(
left: 0,
right: 0,
top: 0,
height: 10,
child: ListView(
children: const <Widget>[
SizedBox(height: 3), // Clips B so it's only 7 pixels tall.
Text('B'),
],
),
);
},
child: const Text('A'),
),
),
],
),
);
},
),
],
),
);
await tester.pumpWidget(widget);
final SemanticsNode clippedOverlayPortal = semantics.nodesWith(label: 'A').single;
final SemanticsNode clippedOverlayChild = semantics.nodesWith(label: 'B').single;
expect(clippedOverlayPortal.rect, Offset.zero & const Size(800, 5));
expect(clippedOverlayChild.rect, Offset.zero & const Size(800, 7));
expect(clippedOverlayPortal.transform, isNull);
// The parent SemanticsNode is created by the ListView.
expect(clippedOverlayChild.transform, Matrix4.translationValues(0.0, 3.0, 0.0));
semantics.dispose();
});
testWidgets("OverlayPortal's semantics node is hidden", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(fontSize: 10),
child: ListView(
children: <Widget>[
// Clips OverlayPortal, making it completely invisible.
SizedBox(height: TestSemantics.fullScreen.height),
Semantics(
container: true,
explicitChildNodes: true,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return const Positioned(
left: 0,
top: 0,
child: Text('B'),
);
},
child: const Text('A'),
),
),
],
),
);
},
),
],
),
);
await tester.pumpWidget(widget);
final SemanticsNode clippedOverlayPortal = semantics.nodesWith(label: 'A').single;
final SemanticsNode clippedOverlayChild = semantics.nodesWith(label: 'B').single;
expect(clippedOverlayPortal.rect, Offset.zero & const Size(800, 10));
expect(clippedOverlayChild.rect, Offset.zero & const Size(10, 10));
expect(clippedOverlayPortal.transform, isNull);
// The parent SemanticsNode is created by OverlayPortal.
expect(clippedOverlayChild.transform, Matrix4.translationValues(0.0, -600.0, 0.0));
semantics.dispose();
});
testWidgets("OverlayPortal's semantics node is dropped but the element is kept alive", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final ScrollController controller = ScrollController(initialScrollOffset: 10);
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(fontSize: 10),
child: ListView(
controller: controller,
cacheExtent: 0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
children: <Widget>[
// Clips OverlayPortal, making it completely invisible.
SizedBox(height: TestSemantics.fullScreen.height),
KeepAlive(
keepAlive: true,
child: Semantics(
container: true,
explicitChildNodes: true,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return const Positioned(
left: 0,
top: 0,
child: Text('B'),
);
},
child: const Text('A'),
),
),
),
],
),
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(semantics.nodesWith(label: 'A'), isNotEmpty);
expect(semantics.nodesWith(label: 'B'), isNotEmpty);
controller.jumpTo(0);
await tester.pump();
expect(semantics.nodesWith(label: 'A'), isEmpty);
expect(semantics.nodesWith(label: 'B'), isEmpty);
semantics.dispose();
final RenderObject overlayRenderObject = tester.renderObject(find.byType(Overlay));
// Paints 'B' but not both 'A' and 'B'.
expect(overlayRenderObject, paints..paragraph());
expect(overlayRenderObject, isNot(paints..paragraph()..paragraph()));
});
});
}
class OverlayStatefulEntry extends OverlayEntry {
......
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