mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Reland "Introduce the PipelineOwner tree (#122231)"
This commit is contained in:
parent
53a1823f0e
commit
12ef7535a8
@ -30,7 +30,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
||||
super.initInstances();
|
||||
_instance = this;
|
||||
_pipelineOwner = PipelineOwner(
|
||||
onNeedVisualUpdate: ensureVisualUpdate,
|
||||
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
|
||||
onSemanticsUpdate: _handleSemanticsUpdate,
|
||||
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
|
||||
@ -45,8 +44,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
||||
if (kIsWeb) {
|
||||
addPostFrameCallback(_handleWebFirstFrame);
|
||||
}
|
||||
addSemanticsEnabledListener(_handleSemanticsEnabledChanged);
|
||||
_handleSemanticsEnabledChanged();
|
||||
_pipelineOwner.attach(_manifold);
|
||||
}
|
||||
|
||||
/// The current [RendererBinding], if one has been created.
|
||||
@ -201,6 +199,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
||||
}
|
||||
}
|
||||
|
||||
late final PipelineManifold _manifold = _BindingPipelineManifold(this);
|
||||
|
||||
/// Creates a [RenderView] object to be the root of the
|
||||
/// [RenderObject] rendering tree, and initializes it so that it
|
||||
/// will be rendered when the next frame is requested.
|
||||
@ -330,17 +330,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
||||
super.dispatchEvent(event, hitTestResult);
|
||||
}
|
||||
|
||||
SemanticsHandle? _semanticsHandle;
|
||||
|
||||
void _handleSemanticsEnabledChanged() {
|
||||
if (semanticsEnabled) {
|
||||
_semanticsHandle ??= _pipelineOwner.ensureSemantics();
|
||||
} else {
|
||||
_semanticsHandle?.dispose();
|
||||
_semanticsHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performSemanticsAction(SemanticsActionEvent action) {
|
||||
_pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
|
||||
@ -621,3 +610,26 @@ class RenderingFlutterBinding extends BindingBase with GestureBinding, Scheduler
|
||||
return RendererBinding.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// A [PipelineManifold] implementation that is backed by the [RendererBinding].
|
||||
class _BindingPipelineManifold extends ChangeNotifier implements PipelineManifold {
|
||||
_BindingPipelineManifold(this._binding) {
|
||||
_binding.addSemanticsEnabledListener(notifyListeners);
|
||||
}
|
||||
|
||||
final RendererBinding _binding;
|
||||
|
||||
@override
|
||||
void requestVisualUpdate() {
|
||||
_binding.ensureVisualUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get semanticsEnabled => _binding.semanticsEnabled;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_binding.removeSemanticsEnabledListener(notifyListeners);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -859,6 +859,20 @@ class _LocalSemanticsHandle implements SemanticsHandle {
|
||||
/// are visible on screen. You can create other pipeline owners to manage
|
||||
/// off-screen objects, which can flush their pipelines independently of the
|
||||
/// on-screen render objects.
|
||||
///
|
||||
/// [PipelineOwner]s can be organized in a tree to manage multiple render trees,
|
||||
/// where each [PipelineOwner] is responsible for one of the render trees. To
|
||||
/// build or modify the tree, call [adoptChild] or [dropChild]. During each of
|
||||
/// the different flush phases described above, a [PipelineOwner] will first
|
||||
/// perform the phase on the nodes it manages in its own render tree before
|
||||
/// calling the same flush method on its children. No assumption must be made
|
||||
/// about the order in which child [PipelineOwner]s are flushed.
|
||||
///
|
||||
/// A [PipelineOwner] may also be [attach]ed to a [PipelineManifold], which
|
||||
/// gives it access to platform functionality usually exposed by the bindings
|
||||
/// without tying it to a specific binding implementation. All [PipelineOwner]s
|
||||
/// in a given tree must be attached to the same [PipelineManifold]. This
|
||||
/// happens automatically during [adoptChild].
|
||||
class PipelineOwner {
|
||||
/// Creates a pipeline owner.
|
||||
///
|
||||
@ -879,6 +893,10 @@ class PipelineOwner {
|
||||
/// various stages of the pipeline. This function might be called multiple
|
||||
/// times in quick succession. Implementations should take care to discard
|
||||
/// duplicate calls quickly.
|
||||
///
|
||||
/// When the [PipelineOwner] is attached to a [PipelineManifold] and
|
||||
/// [onNeedVisualUpdate] is provided, the [onNeedVisualUpdate] callback is
|
||||
/// invoked instead of calling [PipelineManifold.requestVisualUpdate].
|
||||
final VoidCallback? onNeedVisualUpdate;
|
||||
|
||||
/// Called whenever this pipeline owner creates a semantics object.
|
||||
@ -903,7 +921,11 @@ class PipelineOwner {
|
||||
/// Used to notify the pipeline owner that an associated render object wishes
|
||||
/// to update its visual appearance.
|
||||
void requestVisualUpdate() {
|
||||
onNeedVisualUpdate?.call();
|
||||
if (onNeedVisualUpdate != null) {
|
||||
onNeedVisualUpdate!();
|
||||
} else {
|
||||
_manifold?.requestVisualUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// The unique object managed by this pipeline that has no parent.
|
||||
@ -945,6 +967,7 @@ class PipelineOwner {
|
||||
/// always returns false.
|
||||
bool get debugDoingLayout => _debugDoingLayout;
|
||||
bool _debugDoingLayout = false;
|
||||
bool _debugDoingChildLayout = false;
|
||||
|
||||
/// Update the layout information for all dirty render objects.
|
||||
///
|
||||
@ -997,10 +1020,20 @@ class PipelineOwner {
|
||||
// relayout boundary back.
|
||||
_shouldMergeDirtyNodes = false;
|
||||
}
|
||||
|
||||
assert(() {
|
||||
_debugDoingChildLayout = true;
|
||||
return true;
|
||||
}());
|
||||
for (final PipelineOwner child in _children) {
|
||||
child.flushLayout();
|
||||
}
|
||||
assert(_nodesNeedingLayout.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.');
|
||||
} finally {
|
||||
_shouldMergeDirtyNodes = false;
|
||||
assert(() {
|
||||
_debugDoingLayout = false;
|
||||
_debugDoingChildLayout = false;
|
||||
return true;
|
||||
}());
|
||||
if (!kReleaseMode) {
|
||||
@ -1052,6 +1085,10 @@ class PipelineOwner {
|
||||
}
|
||||
}
|
||||
_nodesNeedingCompositingBitsUpdate.clear();
|
||||
for (final PipelineOwner child in _children) {
|
||||
child.flushCompositingBits();
|
||||
}
|
||||
assert(_nodesNeedingCompositingBitsUpdate.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.');
|
||||
if (!kReleaseMode) {
|
||||
Timeline.finishSync();
|
||||
}
|
||||
@ -1116,7 +1153,10 @@ class PipelineOwner {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(_nodesNeedingPaint.isEmpty);
|
||||
for (final PipelineOwner child in _children) {
|
||||
child.flushPaint();
|
||||
}
|
||||
assert(_nodesNeedingPaint.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.');
|
||||
} finally {
|
||||
assert(() {
|
||||
_debugDoingPaint = false;
|
||||
@ -1130,11 +1170,13 @@ class PipelineOwner {
|
||||
|
||||
/// The object that is managing semantics for this pipeline owner, if any.
|
||||
///
|
||||
/// An owner is created by [ensureSemantics]. The owner is valid for as long
|
||||
/// there are [SemanticsHandle]s returned by [ensureSemantics] that have not
|
||||
/// yet been disposed. Once the last handle has been disposed, the
|
||||
/// [semanticsOwner] field will revert to null, and the previous owner will be
|
||||
/// disposed.
|
||||
/// An owner is created by [ensureSemantics] or when the [PipelineManifold] to
|
||||
/// which this owner is connected has [PipelineManifold.semanticsEnabled] set
|
||||
/// to true. The owner is valid for as long as
|
||||
/// [PipelineManifold.semanticsEnabled] remains true or while there are
|
||||
/// outstanding [SemanticsHandle]s from calls to [ensureSemantics]. The
|
||||
/// [semanticsOwner] field will revert to null once both conditions are no
|
||||
/// longer met.
|
||||
///
|
||||
/// When [semanticsOwner] is null, the [PipelineOwner] skips all steps
|
||||
/// relating to semantics.
|
||||
@ -1167,23 +1209,28 @@ class PipelineOwner {
|
||||
/// maintaining the semantics tree.
|
||||
SemanticsHandle ensureSemantics({ VoidCallback? listener }) {
|
||||
_outstandingSemanticsHandles += 1;
|
||||
if (_outstandingSemanticsHandles == 1) {
|
||||
assert(_semanticsOwner == null);
|
||||
assert(onSemanticsUpdate != null, 'Attempted to open a semantics handle without an onSemanticsUpdate callback.');
|
||||
_semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!);
|
||||
onSemanticsOwnerCreated?.call();
|
||||
}
|
||||
_updateSemanticsOwner();
|
||||
return _LocalSemanticsHandle._(this, listener);
|
||||
}
|
||||
|
||||
void _updateSemanticsOwner() {
|
||||
if ((_manifold?.semanticsEnabled ?? false) || _outstandingSemanticsHandles > 0) {
|
||||
if (_semanticsOwner == null) {
|
||||
assert(onSemanticsUpdate != null, 'Attempted to enable semantics without configuring an onSemanticsUpdate callback.');
|
||||
_semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!);
|
||||
onSemanticsOwnerCreated?.call();
|
||||
}
|
||||
} else if (_semanticsOwner != null) {
|
||||
_semanticsOwner?.dispose();
|
||||
_semanticsOwner = null;
|
||||
onSemanticsOwnerDisposed?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void _didDisposeSemanticsHandle() {
|
||||
assert(_semanticsOwner != null);
|
||||
_outstandingSemanticsHandles -= 1;
|
||||
if (_outstandingSemanticsHandles == 0) {
|
||||
_semanticsOwner!.dispose();
|
||||
_semanticsOwner = null;
|
||||
onSemanticsOwnerDisposed?.call();
|
||||
}
|
||||
_updateSemanticsOwner();
|
||||
}
|
||||
|
||||
bool _debugDoingSemantics = false;
|
||||
@ -1222,8 +1269,11 @@ class PipelineOwner {
|
||||
}
|
||||
}
|
||||
_semanticsOwner!.sendSemanticsUpdate();
|
||||
for (final PipelineOwner child in _children) {
|
||||
child.flushSemantics();
|
||||
}
|
||||
assert(_nodesNeedingSemantics.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.');
|
||||
} finally {
|
||||
assert(_nodesNeedingSemantics.isEmpty);
|
||||
assert(() {
|
||||
_debugDoingSemantics = false;
|
||||
return true;
|
||||
@ -1233,6 +1283,166 @@ class PipelineOwner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TREE MANAGEMENT
|
||||
|
||||
final Set<PipelineOwner> _children = <PipelineOwner>{};
|
||||
PipelineManifold? _manifold;
|
||||
|
||||
PipelineOwner? _debugParent;
|
||||
bool _debugSetParent(PipelineOwner child, PipelineOwner? parent) {
|
||||
child._debugParent = parent;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Mark this [PipelineOwner] as attached to the given [PipelineManifold].
|
||||
///
|
||||
/// Typically, this is only called directly on the root [PipelineOwner].
|
||||
/// Children are automatically attached to their parent's [PipelineManifold]
|
||||
/// when [adoptChild] is called.
|
||||
void attach(PipelineManifold manifold) {
|
||||
assert(_manifold == null);
|
||||
_manifold = manifold;
|
||||
_manifold!.addListener(_updateSemanticsOwner);
|
||||
_updateSemanticsOwner();
|
||||
|
||||
for (final PipelineOwner child in _children) {
|
||||
child.attach(manifold);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this [PipelineOwner] as detached.
|
||||
///
|
||||
/// Typically, this is only called directly on the root [PipelineOwner].
|
||||
/// Children are automatically detached from their parent's [PipelineManifold]
|
||||
/// when [dropChild] is called.
|
||||
void detach() {
|
||||
assert(_manifold != null);
|
||||
_manifold!.removeListener(_updateSemanticsOwner);
|
||||
_manifold = null;
|
||||
_updateSemanticsOwner();
|
||||
|
||||
for (final PipelineOwner child in _children) {
|
||||
child.detach();
|
||||
}
|
||||
}
|
||||
|
||||
// In theory, child list modifications are also disallowed between
|
||||
// _debugDoingChildrenLayout and _debugDoingPaint as well as between
|
||||
// _debugDoingPaint and _debugDoingSemantics. However, since the associated
|
||||
// flush methods are usually called back to back, this gets us close enough.
|
||||
bool get _debugAllowChildListModifications => !_debugDoingChildLayout && !_debugDoingPaint && !_debugDoingSemantics;
|
||||
|
||||
/// Adds `child` to this [PipelineOwner].
|
||||
///
|
||||
/// During the phases of frame production (see [RendererBinding.drawFrame]),
|
||||
/// the parent [PipelineOwner] will complete a phase for the nodes it owns
|
||||
/// directly before invoking the flush method corresponding to the current
|
||||
/// phase on its child [PipelineOwner]s. For example, during layout, the
|
||||
/// parent [PipelineOwner] will first lay out its own nodes before calling
|
||||
/// [flushLayout] on its children. During paint, it will first paint its own
|
||||
/// nodes before calling [flushPaint] on its children. This order also applies
|
||||
/// for all the other phases.
|
||||
///
|
||||
/// No assumptions must be made about the order in which child
|
||||
/// [PipelineOwner]s are flushed.
|
||||
///
|
||||
/// No new children may be added after the [PipelineOwner] has started calling
|
||||
/// [flushLayout] on any of its children until the end of the current frame.
|
||||
///
|
||||
/// To remove a child, call [dropChild].
|
||||
void adoptChild(PipelineOwner child) {
|
||||
assert(child._debugParent == null);
|
||||
assert(!_children.contains(child));
|
||||
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
|
||||
_children.add(child);
|
||||
assert(_debugSetParent(child, this));
|
||||
if (_manifold != null) {
|
||||
child.attach(_manifold!);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a child [PipelineOwner] previously added via [adoptChild].
|
||||
///
|
||||
/// This node will cease to call the flush methods on the `child` during frame
|
||||
/// production.
|
||||
///
|
||||
/// No children may be removed after the [PipelineOwner] has started calling
|
||||
/// [flushLayout] on any of its children until the end of the current frame.
|
||||
void dropChild(PipelineOwner child) {
|
||||
assert(child._debugParent == this);
|
||||
assert(_children.contains(child));
|
||||
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
|
||||
_children.remove(child);
|
||||
assert(_debugSetParent(child, null));
|
||||
if (_manifold != null) {
|
||||
child.detach();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls `visitor` for each immediate child of this [PipelineOwner].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [adoptChild] to add a child.
|
||||
/// * [dropChild] to remove a child.
|
||||
void visitChildren(PipelineOwnerVisitor visitor) {
|
||||
_children.forEach(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for the callback to [PipelineOwner.visitChildren].
|
||||
///
|
||||
/// The argument is the child being visited.
|
||||
typedef PipelineOwnerVisitor = void Function(PipelineOwner child);
|
||||
|
||||
/// Manages a tree of [PipelineOwner]s.
|
||||
///
|
||||
/// All [PipelineOwner]s within a tree are attached to the same
|
||||
/// [PipelineManifold], which gives them access to shared functionality such
|
||||
/// as requesting a visual update (by calling [requestVisualUpdate]). As such,
|
||||
/// the [PipelineManifold] gives the [PipelineOwner]s access to functionality
|
||||
/// usually provided by the bindings without tying the [PipelineOwner]s to a
|
||||
/// particular binding implementation.
|
||||
///
|
||||
/// The root of the [PipelineOwner] tree is attached to a [PipelineManifold] by
|
||||
/// passing the manifold to [PipelineOwner.attach]. Children are attached to the
|
||||
/// same [PipelineManifold] as their parent when they are adopted via
|
||||
/// [PipelineOwner.adoptChild].
|
||||
///
|
||||
/// [PipelineOwner]s can register listeners with the [PipelineManifold] to be
|
||||
/// informed when certain values provided by the [PipelineManifold] change.
|
||||
abstract class PipelineManifold implements Listenable {
|
||||
/// Whether [PipelineOwner]s connected to this [PipelineManifold] should
|
||||
/// collect semantics information and produce a semantics tree.
|
||||
///
|
||||
/// The [PipelineManifold] notifies its listeners (managed with [addListener]
|
||||
/// and [removeListener]) when this property changes its value.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SemanticsBinding.semanticsEnabled], which [PipelineManifold]
|
||||
/// implementations typically use to back this property.
|
||||
bool get semanticsEnabled;
|
||||
|
||||
/// Called by a [PipelineOwner] connected to this [PipelineManifold] when a
|
||||
/// [RenderObject] associated with that pipeline owner wishes to update its
|
||||
/// visual appearance.
|
||||
///
|
||||
/// Typical implementations of this function will schedule a task to flush the
|
||||
/// various stages of the pipeline. This function might be called multiple
|
||||
/// times in quick succession. Implementations should take care to discard
|
||||
/// duplicate calls quickly.
|
||||
///
|
||||
/// A [PipelineOwner] connected to this [PipelineManifold] will call
|
||||
/// [PipelineOwner.onNeedVisualUpdate] instead of this method if it has been
|
||||
/// configured with a non-null [PipelineOwner.onNeedVisualUpdate] callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SchedulerBinding.ensureVisualUpdate], which [PipelineManifold]
|
||||
/// implementations typically call to implement this method.
|
||||
void requestVisualUpdate();
|
||||
}
|
||||
|
||||
const String _flutterRenderingLibrary = 'package:flutter/rendering.dart';
|
||||
|
@ -0,0 +1,25 @@
|
||||
// Copyright 2014 The Flutter 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/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('Initializing the RendererBinding does not crash when semantics is enabled', () {
|
||||
try {
|
||||
MyRenderingFlutterBinding();
|
||||
} catch (e) {
|
||||
fail('Initializing the RenderingBinding threw an unexpected error:\n$e');
|
||||
}
|
||||
expect(RendererBinding.instance, isA<MyRenderingFlutterBinding>());
|
||||
expect(SemanticsBinding.instance.semanticsEnabled, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
// Binding that pretends the platform had semantics enabled before the binding
|
||||
// is initialized.
|
||||
class MyRenderingFlutterBinding extends RenderingFlutterBinding {
|
||||
@override
|
||||
bool get semanticsEnabled => true;
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// Copyright 2014 The Flutter 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/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'rendering_tester.dart';
|
||||
|
||||
void main() {
|
||||
MyTestRenderingFlutterBinding.ensureInitialized();
|
||||
|
||||
tearDown(() {
|
||||
final List<PipelineOwner> children = <PipelineOwner>[];
|
||||
RendererBinding.instance.pipelineOwner.visitChildren((PipelineOwner child) {
|
||||
children.add(child);
|
||||
});
|
||||
children.forEach(RendererBinding.instance.pipelineOwner.dropChild);
|
||||
});
|
||||
|
||||
test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () {
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
RendererBinding.instance.pipelineOwner.adoptChild(child);
|
||||
|
||||
final RenderObject renderObject = TestRenderObject();
|
||||
child.rootNode = renderObject;
|
||||
renderObject.scheduleInitialLayout();
|
||||
RendererBinding.instance.pipelineOwner.flushLayout();
|
||||
|
||||
MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0;
|
||||
renderObject.markNeedsLayout();
|
||||
expect(MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount, 1);
|
||||
});
|
||||
|
||||
test('Turning global semantics on/off creates semantics owners in PipelineOwner tree', () {
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
RendererBinding.instance.pipelineOwner.adoptChild(child);
|
||||
|
||||
expect(child.semanticsOwner, isNull);
|
||||
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull);
|
||||
|
||||
final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics();
|
||||
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNotNull);
|
||||
|
||||
handle.dispose();
|
||||
|
||||
expect(child.semanticsOwner, isNull);
|
||||
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull);
|
||||
});
|
||||
}
|
||||
|
||||
class MyTestRenderingFlutterBinding extends TestRenderingFlutterBinding {
|
||||
static MyTestRenderingFlutterBinding get instance => BindingBase.checkInstance(_instance);
|
||||
static MyTestRenderingFlutterBinding? _instance;
|
||||
|
||||
static MyTestRenderingFlutterBinding ensureInitialized() {
|
||||
if (_instance != null) {
|
||||
return _instance!;
|
||||
}
|
||||
return MyTestRenderingFlutterBinding();
|
||||
}
|
||||
|
||||
@override
|
||||
void initInstances() {
|
||||
super.initInstances();
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
int ensureVisualUpdateCount = 0;
|
||||
|
||||
@override
|
||||
void ensureVisualUpdate() {
|
||||
super.ensureVisualUpdate();
|
||||
ensureVisualUpdateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderObject extends RenderObject {
|
||||
@override
|
||||
void debugAssertDoesMeetConstraints() { }
|
||||
|
||||
@override
|
||||
Rect get paintBounds => Rect.zero;
|
||||
|
||||
@override
|
||||
void performLayout() { }
|
||||
|
||||
@override
|
||||
void performResize() { }
|
||||
|
||||
@override
|
||||
Rect get semanticBounds => Rect.zero;
|
||||
}
|
862
packages/flutter/test/rendering/pipeline_owner_tree_test.dart
Normal file
862
packages/flutter/test/rendering/pipeline_owner_tree_test.dart
Normal file
@ -0,0 +1,862 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
FlutterError.presentError = (FlutterErrorDetails details) {
|
||||
// Make tests fail on FlutterErrors.
|
||||
throw details.exception;
|
||||
};
|
||||
|
||||
test('onNeedVisualUpdate takes precedence over manifold', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
int rootOnNeedVisualUpdateCallCount = 0;
|
||||
final TestRenderObject rootRenderObject = TestRenderObject();
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onNeedVisualUpdate: () {
|
||||
rootOnNeedVisualUpdateCallCount += 1;
|
||||
},
|
||||
);
|
||||
root.rootNode = rootRenderObject;
|
||||
rootRenderObject.scheduleInitialLayout();
|
||||
|
||||
int child1OnNeedVisualUpdateCallCount = 0;
|
||||
final TestRenderObject child1RenderObject = TestRenderObject();
|
||||
final PipelineOwner child1 = PipelineOwner(
|
||||
onNeedVisualUpdate: () {
|
||||
child1OnNeedVisualUpdateCallCount += 1;
|
||||
},
|
||||
);
|
||||
child1.rootNode = child1RenderObject;
|
||||
child1RenderObject.scheduleInitialLayout();
|
||||
|
||||
final TestRenderObject child2RenderObject = TestRenderObject();
|
||||
final PipelineOwner child2 = PipelineOwner();
|
||||
child2.rootNode = child2RenderObject;
|
||||
child2RenderObject.scheduleInitialLayout();
|
||||
|
||||
root.adoptChild(child1);
|
||||
root.adoptChild(child2);
|
||||
root.attach(manifold);
|
||||
root.flushLayout();
|
||||
manifold.requestVisualUpdateCount = 0;
|
||||
|
||||
rootRenderObject.markNeedsLayout();
|
||||
expect(manifold.requestVisualUpdateCount, 0);
|
||||
expect(rootOnNeedVisualUpdateCallCount, 1);
|
||||
expect(child1OnNeedVisualUpdateCallCount, 0);
|
||||
|
||||
child1RenderObject.markNeedsLayout();
|
||||
expect(manifold.requestVisualUpdateCount, 0);
|
||||
expect(rootOnNeedVisualUpdateCallCount, 1);
|
||||
expect(child1OnNeedVisualUpdateCallCount, 1);
|
||||
|
||||
child2RenderObject.markNeedsLayout();
|
||||
expect(manifold.requestVisualUpdateCount, 1);
|
||||
expect(rootOnNeedVisualUpdateCallCount, 1);
|
||||
expect(child1OnNeedVisualUpdateCallCount, 1);
|
||||
});
|
||||
|
||||
test("parent's render objects are laid out before child's render objects", () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
final List<String> log = <String>[];
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject(
|
||||
onLayout: () {
|
||||
log.add('layout parent');
|
||||
},
|
||||
);
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
root.rootNode = rootRenderObject;
|
||||
rootRenderObject.scheduleInitialLayout();
|
||||
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onLayout: () {
|
||||
log.add('layout child');
|
||||
},
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
child.rootNode = childRenderObject;
|
||||
childRenderObject.scheduleInitialLayout();
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
expect(log, isEmpty);
|
||||
|
||||
root.flushLayout();
|
||||
expect(log, <String>['layout parent', 'layout child']);
|
||||
});
|
||||
|
||||
test("child cannot dirty parent's render object during flushLayout", () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject();
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
root.rootNode = rootRenderObject;
|
||||
rootRenderObject.scheduleInitialLayout();
|
||||
|
||||
bool childLayoutExecuted = false;
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onLayout: () {
|
||||
childLayoutExecuted = true;
|
||||
expect(() => rootRenderObject.markNeedsLayout(), throwsFlutterError);
|
||||
},
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
child.rootNode = childRenderObject;
|
||||
childRenderObject.scheduleInitialLayout();
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
|
||||
|
||||
root.flushLayout();
|
||||
expect(childLayoutExecuted, isTrue);
|
||||
});
|
||||
|
||||
test('updates compositing bits on children', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject();
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
root.rootNode = rootRenderObject;
|
||||
rootRenderObject.markNeedsCompositingBitsUpdate();
|
||||
|
||||
final TestRenderObject childRenderObject = TestRenderObject();
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
child.rootNode = childRenderObject;
|
||||
childRenderObject.markNeedsCompositingBitsUpdate();
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
expect(() => rootRenderObject.needsCompositing, throwsAssertionError);
|
||||
expect(() => childRenderObject.needsCompositing, throwsAssertionError);
|
||||
|
||||
root.flushCompositingBits();
|
||||
expect(rootRenderObject.needsCompositing, isTrue);
|
||||
expect(childRenderObject.needsCompositing, isTrue);
|
||||
});
|
||||
|
||||
test("parent's render objects are painted before child's render objects", () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
final List<String> log = <String>[];
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject(
|
||||
onPaint: () {
|
||||
log.add('paint parent');
|
||||
},
|
||||
);
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
root.rootNode = rootRenderObject;
|
||||
final OffsetLayer rootLayer = OffsetLayer();
|
||||
rootLayer.attach(rootRenderObject);
|
||||
rootRenderObject.scheduleInitialLayout();
|
||||
rootRenderObject.scheduleInitialPaint(rootLayer);
|
||||
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onPaint: () {
|
||||
log.add('paint child');
|
||||
},
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
child.rootNode = childRenderObject;
|
||||
final OffsetLayer childLayer = OffsetLayer();
|
||||
childLayer.attach(childRenderObject);
|
||||
childRenderObject.scheduleInitialLayout();
|
||||
childRenderObject.scheduleInitialPaint(childLayer);
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
root.flushLayout(); // Can't paint with invalid layout.
|
||||
expect(log, isEmpty);
|
||||
|
||||
root.flushPaint();
|
||||
expect(log, <String>['paint parent', 'paint child']);
|
||||
});
|
||||
|
||||
test("child paint cannot dirty parent's render object", () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject();
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
root.rootNode = rootRenderObject;
|
||||
final OffsetLayer rootLayer = OffsetLayer();
|
||||
rootLayer.attach(rootRenderObject);
|
||||
rootRenderObject.scheduleInitialLayout();
|
||||
rootRenderObject.scheduleInitialPaint(rootLayer);
|
||||
|
||||
bool childPaintExecuted = false;
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onPaint: () {
|
||||
childPaintExecuted = true;
|
||||
expect(() => rootRenderObject.markNeedsPaint(), throwsAssertionError);
|
||||
},
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
child.rootNode = childRenderObject;
|
||||
final OffsetLayer childLayer = OffsetLayer();
|
||||
childLayer.attach(childRenderObject);
|
||||
childRenderObject.scheduleInitialLayout();
|
||||
childRenderObject.scheduleInitialPaint(childLayer);
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
root.flushLayout(); // Can't paint with invalid layout.
|
||||
root.flushPaint();
|
||||
expect(childPaintExecuted, isTrue);
|
||||
});
|
||||
|
||||
test("parent's render objects do semantics before child's render objects", () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold()
|
||||
..semanticsEnabled = true;
|
||||
final List<String> log = <String>[];
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject(
|
||||
onSemantics: () {
|
||||
log.add('semantics parent');
|
||||
},
|
||||
);
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
rootRenderObject.scheduleInitialSemantics();
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
);
|
||||
root.rootNode = rootRenderObject;
|
||||
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onSemantics: () {
|
||||
log.add('semantics child');
|
||||
},
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
childRenderObject.scheduleInitialSemantics();
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
);
|
||||
child.rootNode = childRenderObject;
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
log.clear();
|
||||
|
||||
rootRenderObject.markNeedsSemanticsUpdate();
|
||||
childRenderObject.markNeedsSemanticsUpdate();
|
||||
root.flushSemantics();
|
||||
expect(log, <String>['semantics parent', 'semantics child']);
|
||||
});
|
||||
|
||||
test("child cannot mark parent's render object dirty during flushSemantics", () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold()
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject();
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
rootRenderObject.scheduleInitialSemantics();
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
);
|
||||
root.rootNode = rootRenderObject;
|
||||
|
||||
bool childSemanticsCalled = false;
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onSemantics: () {
|
||||
childSemanticsCalled = true;
|
||||
rootRenderObject.markNeedsSemanticsUpdate();
|
||||
},
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
childRenderObject.scheduleInitialSemantics();
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
);
|
||||
child.rootNode = childRenderObject;
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
rootRenderObject.markNeedsSemanticsUpdate();
|
||||
childRenderObject.markNeedsSemanticsUpdate();
|
||||
root.flushSemantics();
|
||||
|
||||
expect(childSemanticsCalled, isTrue);
|
||||
});
|
||||
|
||||
test('when manifold enables semantics all PipelineOwners in tree create SemanticsOwner', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
int rootOnSemanticsOwnerCreatedCount = 0;
|
||||
int rootOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
rootOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
rootOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
int childOnSemanticsOwnerCreatedCount = 0;
|
||||
int childOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
childOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
childOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 0);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 0);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
|
||||
manifold.semanticsEnabled = true;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
manifold.semanticsEnabled = false;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 1);
|
||||
expect(childOnSemanticsOwnerDisposed, 1);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
});
|
||||
|
||||
test('when manifold enables semantics all PipelineOwners in tree that did not have a SemanticsOwner create one', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
int rootOnSemanticsOwnerCreatedCount = 0;
|
||||
int rootOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
rootOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
rootOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
int childOnSemanticsOwnerCreatedCount = 0;
|
||||
int childOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
childOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
childOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
|
||||
final SemanticsHandle childSemantics = child.ensureSemantics();
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 0);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
manifold.semanticsEnabled = true;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
manifold.semanticsEnabled = false;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 1);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
childSemantics.dispose();
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 1);
|
||||
expect(childOnSemanticsOwnerDisposed, 1);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
});
|
||||
|
||||
test('PipelineOwner can dispose local handle even when manifold forces semantics to on', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
int rootOnSemanticsOwnerCreatedCount = 0;
|
||||
int rootOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
rootOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
rootOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
int childOnSemanticsOwnerCreatedCount = 0;
|
||||
int childOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
childOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
childOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
|
||||
final SemanticsHandle childSemantics = child.ensureSemantics();
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 0);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
manifold.semanticsEnabled = true;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
childSemantics.dispose();
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
manifold.semanticsEnabled = false;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 1);
|
||||
expect(childOnSemanticsOwnerDisposed, 1);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
});
|
||||
|
||||
test('can hold on to local handle when manifold turns off semantics', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
int rootOnSemanticsOwnerCreatedCount = 0;
|
||||
int rootOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
rootOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
rootOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
int childOnSemanticsOwnerCreatedCount = 0;
|
||||
int childOnSemanticsOwnerDisposed = 0;
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsOwnerCreated: () {
|
||||
childOnSemanticsOwnerCreatedCount++;
|
||||
},
|
||||
onSemanticsUpdate: (SemanticsUpdate update) { },
|
||||
onSemanticsOwnerDisposed: () {
|
||||
childOnSemanticsOwnerDisposed++;
|
||||
},
|
||||
);
|
||||
|
||||
root.adoptChild(child);
|
||||
root.attach(manifold);
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 0);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 0);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
|
||||
manifold.semanticsEnabled = true;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
final SemanticsHandle childSemantics = child.ensureSemantics();
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 0);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
manifold.semanticsEnabled = false;
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 1);
|
||||
expect(childOnSemanticsOwnerDisposed, 0);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
|
||||
childSemantics.dispose();
|
||||
|
||||
expect(rootOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(childOnSemanticsOwnerCreatedCount, 1);
|
||||
expect(rootOnSemanticsOwnerDisposed, 1);
|
||||
expect(childOnSemanticsOwnerDisposed, 1);
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
});
|
||||
|
||||
test('cannot attach when already attached', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
final PipelineOwner owner = PipelineOwner();
|
||||
|
||||
owner.attach(manifold);
|
||||
expect(() => owner.attach(manifold), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('attach update semanticsOwner', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold()
|
||||
..semanticsEnabled = true;
|
||||
final PipelineOwner owner = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
|
||||
expect(owner.semanticsOwner, isNull);
|
||||
owner.attach(manifold);
|
||||
expect(owner.semanticsOwner, isNotNull);
|
||||
});
|
||||
|
||||
test('attach does not request visual update if nothing is dirty', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
final TestRenderObject renderObject = TestRenderObject();
|
||||
final PipelineOwner owner = PipelineOwner();
|
||||
owner.rootNode = renderObject;
|
||||
|
||||
expect(manifold.requestVisualUpdateCount, 0);
|
||||
owner.attach(manifold);
|
||||
expect(manifold.requestVisualUpdateCount, 0);
|
||||
});
|
||||
|
||||
test('cannot detach when not attached', () {
|
||||
final PipelineOwner owner = PipelineOwner();
|
||||
|
||||
expect(() => owner.detach(), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('cannot adopt twice', () {
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
root.adoptChild(child);
|
||||
expect(() => root.adoptChild(child), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('cannot adopt child of other parent', () {
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
final PipelineOwner otherRoot = PipelineOwner();
|
||||
root.adoptChild(child);
|
||||
expect(() => otherRoot.adoptChild(child), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('adopting creates semantics owner if necessary', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
final PipelineOwner childOfChild = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
root.attach(manifold);
|
||||
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
expect(childOfChild.semanticsOwner, isNull);
|
||||
|
||||
root.adoptChild(child);
|
||||
|
||||
expect(root.semanticsOwner, isNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
expect(childOfChild.semanticsOwner, isNull);
|
||||
|
||||
manifold.semanticsEnabled = true;
|
||||
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
expect(childOfChild.semanticsOwner, isNull);
|
||||
|
||||
child.adoptChild(childOfChild);
|
||||
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
expect(childOfChild.semanticsOwner, isNotNull);
|
||||
});
|
||||
|
||||
test('cannot drop unattached child', () {
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
expect(() => root.dropChild(child), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('cannot drop child attached to other parent', () {
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child = PipelineOwner();
|
||||
final PipelineOwner otherRoot = PipelineOwner();
|
||||
otherRoot.adoptChild(child);
|
||||
expect(() => root.dropChild(child), throwsAssertionError);
|
||||
});
|
||||
|
||||
test('dropping destroys semantics owner if necessary', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold()
|
||||
..semanticsEnabled = true;
|
||||
final PipelineOwner root = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
final PipelineOwner child = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
final PipelineOwner childOfChild = PipelineOwner(
|
||||
onSemanticsUpdate: (_) { },
|
||||
);
|
||||
root.attach(manifold);
|
||||
root.adoptChild(child);
|
||||
child.adoptChild(childOfChild);
|
||||
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
expect(childOfChild.semanticsOwner, isNotNull);
|
||||
|
||||
child.dropChild(childOfChild);
|
||||
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
expect(childOfChild.semanticsOwner, isNull);
|
||||
|
||||
final SemanticsHandle childSemantics = child.ensureSemantics();
|
||||
root.dropChild(child);
|
||||
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNotNull);
|
||||
expect(childOfChild.semanticsOwner, isNull);
|
||||
|
||||
childSemantics.dispose();
|
||||
|
||||
expect(root.semanticsOwner, isNotNull);
|
||||
expect(child.semanticsOwner, isNull);
|
||||
expect(childOfChild.semanticsOwner, isNull);
|
||||
});
|
||||
|
||||
test('can adopt/drop children during own layout', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child1 = PipelineOwner();
|
||||
final PipelineOwner child2 = PipelineOwner();
|
||||
|
||||
final TestRenderObject rootRenderObject = TestRenderObject(
|
||||
onLayout: () {
|
||||
child1.dropChild(child2);
|
||||
root.dropChild(child1);
|
||||
root.adoptChild(child2);
|
||||
child2.adoptChild(child1);
|
||||
},
|
||||
);
|
||||
|
||||
root.rootNode = rootRenderObject;
|
||||
rootRenderObject.scheduleInitialLayout();
|
||||
|
||||
root.adoptChild(child1);
|
||||
child1.adoptChild(child2);
|
||||
root.attach(manifold);
|
||||
expect(_treeWalk(root), <PipelineOwner>[root, child1, child2]);
|
||||
|
||||
root.flushLayout();
|
||||
|
||||
expect(_treeWalk(root), <PipelineOwner>[root, child2, child1]);
|
||||
});
|
||||
|
||||
test('cannot adopt/drop children during child layout', () {
|
||||
final TestPipelineManifold manifold = TestPipelineManifold();
|
||||
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child1 = PipelineOwner();
|
||||
final PipelineOwner child2 = PipelineOwner();
|
||||
final PipelineOwner child3 = PipelineOwner();
|
||||
|
||||
Object? droppingError;
|
||||
Object? adoptingError;
|
||||
|
||||
final TestRenderObject childRenderObject = TestRenderObject(
|
||||
onLayout: () {
|
||||
child1.dropChild(child2);
|
||||
child1.adoptChild(child3);
|
||||
try {
|
||||
root.dropChild(child1);
|
||||
} catch (e) {
|
||||
droppingError = e;
|
||||
}
|
||||
try {
|
||||
root.adoptChild(child2);
|
||||
} catch (e) {
|
||||
adoptingError = e;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
child1.rootNode = childRenderObject;
|
||||
childRenderObject.scheduleInitialLayout();
|
||||
|
||||
root.adoptChild(child1);
|
||||
child1.adoptChild(child2);
|
||||
root.attach(manifold);
|
||||
expect(_treeWalk(root), <PipelineOwner>[root, child1, child2]);
|
||||
|
||||
root.flushLayout();
|
||||
|
||||
expect(adoptingError, isAssertionError.having((AssertionError e) => e.message, 'message', contains('Cannot modify child list after layout.')));
|
||||
expect(droppingError, isAssertionError.having((AssertionError e) => e.message, 'message', contains('Cannot modify child list after layout.')));
|
||||
});
|
||||
|
||||
test('visitChildren visits all children', () {
|
||||
final PipelineOwner root = PipelineOwner();
|
||||
final PipelineOwner child1 = PipelineOwner();
|
||||
final PipelineOwner child2 = PipelineOwner();
|
||||
final PipelineOwner child3 = PipelineOwner();
|
||||
final PipelineOwner childOfChild3 = PipelineOwner();
|
||||
|
||||
root.adoptChild(child1);
|
||||
root.adoptChild(child2);
|
||||
root.adoptChild(child3);
|
||||
child3.adoptChild(childOfChild3);
|
||||
|
||||
final List<PipelineOwner> children = <PipelineOwner>[];
|
||||
root.visitChildren((PipelineOwner child) {
|
||||
children.add(child);
|
||||
});
|
||||
expect(children, <PipelineOwner>[child1, child2, child3]);
|
||||
|
||||
children.clear();
|
||||
child3.visitChildren((PipelineOwner child) {
|
||||
children.add(child);
|
||||
});
|
||||
expect(children.single, childOfChild3);
|
||||
});
|
||||
}
|
||||
|
||||
class TestPipelineManifold extends ChangeNotifier implements PipelineManifold {
|
||||
int requestVisualUpdateCount = 0;
|
||||
|
||||
@override
|
||||
void requestVisualUpdate() {
|
||||
requestVisualUpdateCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get semanticsEnabled => _semanticsEnabled;
|
||||
bool _semanticsEnabled = false;
|
||||
set semanticsEnabled(bool value) {
|
||||
if (value == _semanticsEnabled) {
|
||||
return;
|
||||
}
|
||||
_semanticsEnabled = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderObject extends RenderObject {
|
||||
TestRenderObject({this.onLayout, this.onPaint, this.onSemantics});
|
||||
|
||||
final VoidCallback? onLayout;
|
||||
final VoidCallback? onPaint;
|
||||
final VoidCallback? onSemantics;
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
|
||||
@override
|
||||
void debugAssertDoesMeetConstraints() { }
|
||||
|
||||
@override
|
||||
Rect get paintBounds => Rect.zero;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
onLayout?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
onPaint?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
onSemantics?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void performResize() { }
|
||||
|
||||
@override
|
||||
Rect get semanticBounds => Rect.zero;
|
||||
}
|
||||
|
||||
List<PipelineOwner> _treeWalk(PipelineOwner root) {
|
||||
final List<PipelineOwner> results = <PipelineOwner>[root];
|
||||
|
||||
void visitor(PipelineOwner child) {
|
||||
results.add(child);
|
||||
child.visitChildren(visitor);
|
||||
}
|
||||
|
||||
root.visitChildren(visitor);
|
||||
return results;
|
||||
}
|
@ -1045,7 +1045,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
||||
void _verifySemanticsHandlesWereDisposed() {
|
||||
assert(_lastRecordedSemanticsHandles != null);
|
||||
// TODO(goderbauer): Fix known leak in web engine when running integration tests and remove this "correction", https://github.com/flutter/flutter/issues/121640.
|
||||
final int knownWebEngineLeakForLiveTestsCorrection = kIsWeb && binding is LiveTestWidgetsFlutterBinding ? 2 : 0;
|
||||
final int knownWebEngineLeakForLiveTestsCorrection = kIsWeb && binding is LiveTestWidgetsFlutterBinding ? 1 : 0;
|
||||
|
||||
if (_currentSemanticsHandles - knownWebEngineLeakForLiveTestsCorrection > _lastRecordedSemanticsHandles!) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
|
Loading…
Reference in New Issue
Block a user