Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

6480 lines
242 KiB
Dart

// 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.
/// @docImport 'dart:ui';
///
/// @docImport 'package:flutter/widgets.dart';
///
/// @docImport 'box.dart';
/// @docImport 'paragraph.dart';
/// @docImport 'proxy_box.dart';
/// @docImport 'view.dart';
/// @docImport 'viewport.dart';
library;
import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
import 'binding.dart';
import 'debug.dart';
import 'layer.dart';
export 'package:flutter/foundation.dart'
show
DiagnosticPropertiesBuilder,
DiagnosticsNode,
DiagnosticsProperty,
DoubleProperty,
EnumProperty,
ErrorDescription,
ErrorHint,
ErrorSummary,
FlagProperty,
FlutterError,
InformationCollector,
IntProperty,
StringProperty;
export 'package:flutter/gestures.dart' show HitTestEntry, HitTestResult;
export 'package:flutter/painting.dart';
/// Base class for data associated with a [RenderObject] by its parent.
///
/// Some render objects wish to store data on their children, such as the
/// children's input parameters to the parent's layout algorithm or the
/// children's position relative to other children.
///
/// See also:
///
/// * [RenderObject.setupParentData], which [RenderObject] subclasses may
/// override to attach specific types of parent data to children.
class ParentData {
/// Called when the RenderObject is removed from the tree.
@protected
@mustCallSuper
void detach() {}
@override
String toString() => '<none>';
}
/// Signature for painting into a [PaintingContext].
///
/// The `offset` argument is the offset from the origin of the coordinate system
/// of the [PaintingContext.canvas] to the coordinate system of the callee.
///
/// Used by many of the methods of [PaintingContext].
typedef PaintingContextCallback = void Function(PaintingContext context, Offset offset);
/// Signature for transforming the given [Rect] to a different [Rect] with a
/// [Matrix4] transform.
typedef _TransformRect = Rect Function(Matrix4 transform, Rect rect);
/// A place to paint.
///
/// Rather than holding a canvas directly, [RenderObject]s paint using a painting
/// context. The painting context has a [Canvas], which receives the
/// individual draw operations, and also has functions for painting child
/// render objects.
///
/// When painting a child render object, the canvas held by the painting context
/// can change because the draw operations issued before and after painting the
/// child might be recorded in separate compositing layers. For this reason, do
/// not hold a reference to the canvas across operations that might paint
/// child render objects.
///
/// New [PaintingContext] objects are created automatically when using
/// [PaintingContext.repaintCompositedChild] and [pushLayer].
class PaintingContext extends ClipContext {
/// Creates a painting context.
///
/// Typically only called by [PaintingContext.repaintCompositedChild]
/// and [pushLayer].
@protected
PaintingContext(this._containerLayer, this.estimatedBounds);
final ContainerLayer _containerLayer;
/// An estimate of the bounds within which the painting context's [canvas]
/// will record painting commands. This can be useful for debugging.
///
/// The canvas will allow painting outside these bounds.
///
/// The [estimatedBounds] rectangle is in the [canvas] coordinate system.
final Rect estimatedBounds;
/// Repaint the given render object.
///
/// The render object must be attached to a [PipelineOwner], must have a
/// 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
/// 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}) {
assert(child._needsPaint);
_repaintCompositedChild(child, debugAlsoPaintedParent: debugAlsoPaintedParent);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
assert(child.isRepaintBoundary);
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: debugAlsoPaintedParent,
includedChild: true,
);
return true;
}());
var childLayer = child._layerHandle.layer as OffsetLayer?;
if (childLayer == null) {
assert(debugAlsoPaintedParent);
assert(child._layerHandle.layer == null);
// Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a
// layer for repaint boundaries.
final OffsetLayer layer = child.updateCompositedLayer(oldLayer: null);
child._layerHandle.layer = childLayer = layer;
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
Offset? debugOldOffset;
assert(() {
debugOldOffset = childLayer!.offset;
return true;
}());
childLayer.removeAllChildren();
final OffsetLayer updatedLayer = child.updateCompositedLayer(oldLayer: childLayer);
assert(
identical(updatedLayer, childLayer),
'$child created a new layer instance $updatedLayer instead of reusing the '
'existing layer $childLayer. See the documentation of RenderObject.updateCompositedLayer '
'for more information on how to correctly implement this method.',
);
assert(debugOldOffset == updatedLayer.offset);
}
child._needsCompositedLayerUpdate = false;
assert(identical(childLayer, child._layerHandle.layer));
assert(child._layerHandle.layer is OffsetLayer);
assert(() {
childLayer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
childContext ??= PaintingContext(childLayer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
// Double-check that the paint method did not replace the layer (the first
// check is done in the [layer] setter itself).
assert(identical(childLayer, child._layerHandle.layer));
childContext.stopRecordingIfNeeded();
}
/// Update the composited layer of [child] without repainting its children.
///
/// The render object must be attached to a [PipelineOwner], must have a
/// composited layer, and must be in need of a composited layer update but
/// not in need of painting. The render object's layer is re-used, and none
/// of its children are repaint or their layers updated.
///
/// See also:
///
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
/// has a composited layer.
static void updateLayerProperties(RenderObject child) {
assert(child.isRepaintBoundary && child._wasRepaintBoundary);
assert(!child._needsPaint);
assert(child._layerHandle.layer != null);
final childLayer = child._layerHandle.layer! as OffsetLayer;
Offset? debugOldOffset;
assert(() {
debugOldOffset = childLayer.offset;
return true;
}());
final OffsetLayer updatedLayer = child.updateCompositedLayer(oldLayer: childLayer);
assert(
identical(updatedLayer, childLayer),
'$child created a new layer instance $updatedLayer instead of reusing the '
'existing layer $childLayer. See the documentation of RenderObject.updateCompositedLayer '
'for more information on how to correctly implement this method.',
);
assert(debugOldOffset == updatedLayer.offset);
child._needsCompositedLayerUpdate = false;
}
/// In debug mode, repaint the given render object using a custom painting
/// context that can record the results of the painting operation in addition
/// to performing the regular paint of the child.
///
/// See also:
///
/// * [repaintCompositedChild], for repainting a composited child without
/// instrumentation.
static void debugInstrumentRepaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
required PaintingContext customContext,
}) {
assert(() {
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
childContext: customContext,
);
return true;
}());
}
/// Paint a child [RenderObject].
///
/// If the child has its own composited layer, the child will be composited
/// into the layer subtree associated with this painting context. Otherwise,
/// the child will be painted into the current PictureLayer for this context.
void paintChild(RenderObject child, Offset offset) {
assert(() {
debugOnProfilePaint?.call(child);
return true;
}());
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
// If a render object was a repaint boundary but no longer is one, this
// is where the framework managed layer is automatically disposed.
} else if (child._wasRepaintBoundary) {
assert(child._layerHandle.layer is OffsetLayer);
child._layerHandle.layer = null;
child._paintWithContext(this, offset);
} else {
child._paintWithContext(this, offset);
}
}
void _compositeChild(RenderObject child, Offset offset) {
assert(!_isRecording);
assert(child.isRepaintBoundary);
assert(_canvas == null || _canvas!.getSaveCount() == 1);
// Create a layer for our child, and paint the child into it.
if (child._needsPaint || !child._wasRepaintBoundary) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
if (child._needsCompositedLayerUpdate) {
updateLayerProperties(child);
}
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint();
child._layerHandle.layer!.debugCreator = child.debugCreator ?? child;
return true;
}());
}
assert(child._layerHandle.layer is OffsetLayer);
final childOffsetLayer = child._layerHandle.layer! as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(childOffsetLayer);
}
/// Adds a layer to the recording requiring that the recording is already
/// stopped.
///
/// Do not call this function directly: call [addLayer] or [pushLayer]
/// instead. This function is called internally when all layers not
/// generated from the [canvas] are added.
///
/// Subclasses that need to customize how layers are added should override
/// this method.
@protected
void appendLayer(Layer layer) {
assert(!_isRecording);
layer.remove();
_containerLayer.append(layer);
}
bool get _isRecording {
final hasCanvas = _canvas != null;
assert(() {
if (hasCanvas) {
assert(_currentLayer != null);
assert(_recorder != null);
assert(_canvas != null);
} else {
assert(_currentLayer == null);
assert(_recorder == null);
assert(_canvas == null);
}
return true;
}());
return hasCanvas;
}
// Recording state
PictureLayer? _currentLayer;
ui.PictureRecorder? _recorder;
Canvas? _canvas;
/// The recorder that is being used by this [PaintingContext]
/// to record interactions with the [Canvas].
///
/// It's fragile to hold a reference to the recorder
/// returned by this getter as it can change at any time.
ui.PictureRecorder get recorder {
if (_recorder == null) {
_startRecording();
}
assert(_currentLayer != null);
return _recorder!;
}
/// The canvas on which to paint.
///
/// 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
/// returned by this getter.
@override
Canvas get canvas {
if (_canvas == null) {
_startRecording();
}
assert(_currentLayer != null);
return _canvas!;
}
void _startRecording() {
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);
_recorder = RendererBinding.instance.createPictureRecorder();
_canvas = RendererBinding.instance.createCanvas(_recorder!);
_containerLayer.append(_currentLayer!);
}
/// Adds a [CompositionCallback] for the current [ContainerLayer] used by this
/// context.
///
/// Composition callbacks are called whenever the layer tree containing the
/// current layer of this painting context gets composited, or when it gets
/// detached and will not be rendered again. This happens regardless of
/// whether the layer is added via retained rendering or not.
///
/// {@macro flutter.rendering.Layer.compositionCallbacks}
///
/// See also:
/// * [Layer.addCompositionCallback].
VoidCallback addCompositionCallback(CompositionCallback callback) {
return _containerLayer.addCompositionCallback(callback);
}
/// Stop recording to a canvas if recording has started.
///
/// Do not call this function directly: functions in this class will call
/// this method as needed. This function is called internally to ensure that
/// recording is stopped before adding layers or finalizing the results of a
/// paint.
///
/// Subclasses that need to customize how recording to a canvas is performed
/// should override this method to save the results of the custom canvas
/// recordings.
@protected
@mustCallSuper
void stopRecordingIfNeeded() {
if (!_isRecording) {
return;
}
assert(() {
if (debugRepaintRainbowEnabled) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0
..color = debugCurrentRepaintColor.toColor();
canvas.drawRect(estimatedBounds.deflate(3.0), paint);
}
if (debugPaintLayerBordersEnabled) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFFFF9800);
canvas.drawRect(estimatedBounds, paint);
}
return true;
}());
_currentLayer!.picture = _recorder!.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
/// Hints that the painting in the current layer is complex and would benefit
/// from caching.
///
/// If this hint is not set, the compositor will apply its own heuristics to
/// decide whether the current layer is complex enough to benefit from
/// caching.
///
/// Calling this ensures a [Canvas] is available. Only draw calls on the
/// current canvas will be hinted; the hint is not propagated to new canvases
/// created after a new layer is added to the painting context (e.g. with
/// [addLayer] or [pushLayer]).
void setIsComplexHint() {
if (_currentLayer == null) {
_startRecording();
}
_currentLayer!.isComplexHint = true;
}
/// Hints that the painting in the current layer is likely to change next frame.
///
/// This hint tells the compositor not to cache the current layer because the
/// cache will not be used in the future. If this hint is not set, the
/// compositor will apply its own heuristics to decide whether the current
/// layer is likely to be reused in the future.
///
/// Calling this ensures a [Canvas] is available. Only draw calls on the
/// current canvas will be hinted; the hint is not propagated to new canvases
/// created after a new layer is added to the painting context (e.g. with
/// [addLayer] or [pushLayer]).
void setWillChangeHint() {
if (_currentLayer == null) {
_startRecording();
}
_currentLayer!.willChangeHint = true;
}
/// Adds a composited leaf layer to the recording.
///
/// After calling this function, the [canvas] property will change to refer to
/// a new [Canvas] that draws on top of the given layer.
///
/// A [RenderObject] that uses this function is very likely to require its
/// [RenderObject.alwaysNeedsCompositing] property to return true. That informs
/// ancestor render objects that this render object will include a composited
/// layer, which, for example, causes them to use composited clips.
///
/// See also:
///
/// * [pushLayer], for adding a layer and painting further contents within
/// it.
void addLayer(Layer layer) {
stopRecordingIfNeeded();
appendLayer(layer);
}
/// Appends the given layer to the recording, and calls the `painter` callback
/// with that layer, providing the `childPaintBounds` as the estimated paint
/// bounds of the child. The `childPaintBounds` can be used for debugging but
/// have no effect on painting.
///
/// The given layer must be an unattached orphan. (Providing a newly created
/// object, rather than reusing an existing layer, satisfies that
/// requirement.)
///
/// {@template flutter.rendering.PaintingContext.pushLayer.offset}
/// The `offset` is the offset to pass to the `painter`. In particular, it is
/// not an offset applied to the layer itself. Layers conceptually by default
/// have no position or size, though they can transform their contents. For
/// example, an [OffsetLayer] applies an offset to its children.
/// {@endtemplate}
///
/// 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
/// transformation or clipping to its contents. The `childPaintBounds`, if
/// specified, must be in the coordinate system of the new layer (i.e. as seen
/// by its children after it applies whatever transform to its contents), and
/// should not go outside the current layer's paint bounds.
///
/// See also:
///
/// * [addLayer], for pushing a layer without painting further contents
/// within it.
void pushLayer(
ContainerLayer childLayer,
PaintingContextCallback painter,
Offset offset, {
Rect? childPaintBounds,
}) {
// If a layer is being reused, it may already contain children. We remove
// them so that `painter` can add children that are relevant for this frame.
if (childLayer.hasChildren) {
childLayer.removeAllChildren();
}
stopRecordingIfNeeded();
appendLayer(childLayer);
final PaintingContext childContext = createChildContext(
childLayer,
childPaintBounds ?? estimatedBounds,
);
painter(childContext, offset);
childContext.stopRecordingIfNeeded();
}
/// Creates a painting context configured to paint into [childLayer].
///
/// The `bounds` are estimated paint bounds for debugging purposes.
@protected
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
return PaintingContext(childLayer, bounds);
}
/// Clip further painting using a rectangle.
///
/// {@template flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
/// The `needsCompositing` argument specifies whether the child needs
/// compositing. Typically this matches the value of
/// [RenderObject.needsCompositing] for the caller. If false, this method
/// returns null, indicating that a layer is no longer necessary. If a render
/// object calling this method stores the `oldLayer` in its
/// [RenderObject.layer] field, it should set that field to null.
///
/// When `needsCompositing` is false, this method will use a more efficient
/// way to apply the layer effect than actually creating a layer.
/// {@endtemplate}
///
/// {@template flutter.rendering.PaintingContext.pushClipRect.offset}
/// The `offset` argument is the offset from the origin of the canvas'
/// coordinate system to the origin of the caller's coordinate system.
/// {@endtemplate}
///
/// The `clipRect` is the rectangle (in the caller's coordinate system) to use
/// to clip the painting done by [painter]. It should not include the
/// `offset`.
///
/// The `painter` callback will be called while the `clipRect` is applied. It
/// is called synchronously during the call to [pushClipRect].
///
/// The `clipBehavior` argument controls how the rectangle is clipped.
///
/// {@template flutter.rendering.PaintingContext.pushClipRect.oldLayer}
/// For the `oldLayer` argument, specify the layer created in the previous
/// frame. This gives the engine more information for performance
/// optimizations. Typically this is the value of [RenderObject.layer] that a
/// render object creates once, then reuses for all subsequent frames until a
/// layer is no longer needed (e.g. the render object no longer needs
/// compositing) or until the render object changes the type of the layer
/// (e.g. from opacity layer to a clip rect layer).
/// {@endtemplate}
ClipRectLayer? pushClipRect(
bool needsCompositing,
Offset offset,
Rect clipRect,
PaintingContextCallback painter, {
Clip clipBehavior = Clip.hardEdge,
ClipRectLayer? oldLayer,
}) {
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
}
final Rect offsetClipRect = clipRect.shift(offset);
if (needsCompositing) {
final ClipRectLayer layer = oldLayer ?? ClipRectLayer();
layer
..clipRect = offsetClipRect
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetClipRect);
return layer;
} else {
clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset));
return null;
}
}
/// Clip further painting using a rounded rectangle.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.offset}
///
/// The `bounds` argument is used to specify the region of the canvas (in the
/// caller's coordinate system) into which `painter` will paint.
///
/// The `clipRRect` argument specifies the rounded-rectangle (in the caller's
/// coordinate system) to use to clip the painting done by `painter`. It
/// should not include the `offset`.
///
/// The `painter` callback will be called while the `clipRRect` is applied. It
/// is called synchronously during the call to [pushClipRRect].
///
/// The `clipBehavior` argument controls how the rounded rectangle is clipped.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
ClipRRectLayer? pushClipRRect(
bool needsCompositing,
Offset offset,
Rect bounds,
RRect clipRRect,
PaintingContextCallback painter, {
Clip clipBehavior = Clip.antiAlias,
ClipRRectLayer? oldLayer,
}) {
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
}
final Rect offsetBounds = bounds.shift(offset);
final RRect offsetClipRRect = clipRRect.shift(offset);
if (needsCompositing) {
final ClipRRectLayer layer = oldLayer ?? ClipRRectLayer();
layer
..clipRRect = offsetClipRRect
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
return layer;
} else {
clipRRectAndPaint(offsetClipRRect, clipBehavior, offsetBounds, () => painter(this, offset));
return null;
}
}
/// Clip further painting using a rounded superellipse.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.offset}
///
/// The `bounds` argument is used to specify the region of the canvas (in the
/// caller's coordinate system) into which `painter` will paint.
///
/// The `clipRSuperellipse` argument specifies the rounded-superellipse (in the caller's
/// coordinate system) to use to clip the painting done by `painter`. It
/// should not include the `offset`.
///
/// The `painter` callback will be called while the `clipRSuperellipse` is applied. It
/// is called synchronously during the call to [pushClipRSuperellipse].
///
/// The `clipBehavior` argument controls how the rounded rectangle is clipped.
///
/// Hit tests are performed based on the bounding box of the [RSuperellipse].
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
ClipRSuperellipseLayer? pushClipRSuperellipse(
bool needsCompositing,
Offset offset,
Rect bounds,
RSuperellipse clipRSuperellipse,
PaintingContextCallback painter, {
Clip clipBehavior = Clip.antiAlias,
ClipRSuperellipseLayer? oldLayer,
}) {
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
}
final Rect offsetBounds = bounds.shift(offset);
final RSuperellipse offsetShape = clipRSuperellipse.shift(offset);
if (needsCompositing) {
final ClipRSuperellipseLayer layer = oldLayer ?? ClipRSuperellipseLayer();
layer
..clipRSuperellipse = offsetShape
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
return layer;
} else {
clipRSuperellipseAndPaint(
offsetShape,
clipBehavior,
offsetBounds,
() => painter(this, offset),
);
return null;
}
}
/// Clip further painting using a path.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.offset}
///
/// The `bounds` argument is used to specify the region of the canvas (in the
/// caller's coordinate system) into which `painter` will paint.
///
/// The `clipPath` argument specifies the [Path] (in the caller's coordinate
/// system) to use to clip the painting done by `painter`. It should not
/// include the `offset`.
///
/// The `painter` callback will be called while the `clipPath` is applied. It
/// is called synchronously during the call to [pushClipPath].
///
/// The `clipBehavior` argument controls how the path is clipped.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
ClipPathLayer? pushClipPath(
bool needsCompositing,
Offset offset,
Rect bounds,
Path clipPath,
PaintingContextCallback painter, {
Clip clipBehavior = Clip.antiAlias,
ClipPathLayer? oldLayer,
}) {
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
}
final Rect offsetBounds = bounds.shift(offset);
final Path offsetClipPath = clipPath.shift(offset);
if (needsCompositing) {
final ClipPathLayer layer = oldLayer ?? ClipPathLayer();
layer
..clipPath = offsetClipPath
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
return layer;
} else {
clipPathAndPaint(offsetClipPath, clipBehavior, offsetBounds, () => painter(this, offset));
return null;
}
}
/// Blend further painting with a color filter.
///
/// {@macro flutter.rendering.PaintingContext.pushLayer.offset}
///
/// The `colorFilter` argument is the [ColorFilter] value to use when blending
/// the painting done by `painter`.
///
/// The `painter` callback will be called while the `colorFilter` is applied.
/// It is called synchronously during the call to [pushColorFilter].
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
///
/// A [RenderObject] that uses this function is very likely to require its
/// [RenderObject.alwaysNeedsCompositing] property to return true. That informs
/// ancestor render objects that this render object will include a composited
/// layer, which, for example, causes them to use composited clips.
ColorFilterLayer pushColorFilter(
Offset offset,
ColorFilter colorFilter,
PaintingContextCallback painter, {
ColorFilterLayer? oldLayer,
}) {
final ColorFilterLayer layer = oldLayer ?? ColorFilterLayer();
layer.colorFilter = colorFilter;
pushLayer(layer, painter, offset);
return layer;
}
/// Transform further painting using a matrix.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
///
/// The `offset` argument is the offset to pass to `painter` and the offset to
/// the origin used by `transform`.
///
/// The `transform` argument is the [Matrix4] with which to transform the
/// coordinate system while calling `painter`. It should not include `offset`.
/// It is applied effectively after applying `offset`.
///
/// The `painter` callback will be called while the `transform` is applied. It
/// is called synchronously during the call to [pushTransform].
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
TransformLayer? pushTransform(
bool needsCompositing,
Offset offset,
Matrix4 transform,
PaintingContextCallback painter, {
TransformLayer? oldLayer,
}) {
final effectiveTransform = Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)
..translateByDouble(-offset.dx, -offset.dy, 0, 1);
if (needsCompositing) {
final TransformLayer layer = oldLayer ?? TransformLayer();
layer.transform = effectiveTransform;
pushLayer(
layer,
painter,
offset,
childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, estimatedBounds),
);
return layer;
} else {
canvas
..save()
..transform(effectiveTransform.storage);
painter(this, offset);
canvas.restore();
return null;
}
}
/// Blend further painting with an alpha value.
///
/// The `offset` argument indicates an offset to apply to all the children
/// (the rendering created by `painter`).
///
/// The `alpha` argument is the alpha value to use when blending the painting
/// done by `painter`. An alpha value of 0 means the painting is fully
/// transparent and an alpha value of 255 means the painting is fully opaque.
///
/// The `painter` callback will be called while the `alpha` is applied. It
/// is called synchronously during the call to [pushOpacity].
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
///
/// A [RenderObject] that uses this function is very likely to require its
/// [RenderObject.alwaysNeedsCompositing] property to return true. That informs
/// ancestor render objects that this render object will include a composited
/// layer, which, for example, causes them to use composited clips.
OpacityLayer pushOpacity(
Offset offset,
int alpha,
PaintingContextCallback painter, {
OpacityLayer? oldLayer,
}) {
final OpacityLayer layer = oldLayer ?? OpacityLayer();
layer
..alpha = alpha
..offset = offset;
pushLayer(layer, painter, Offset.zero);
return layer;
}
@override
String toString() =>
'${objectRuntimeType(this, 'PaintingContext')}#$hashCode(layer: $_containerLayer, canvas bounds: $estimatedBounds)';
}
/// An abstract set of layout constraints.
///
/// Concrete layout models (such as box) will create concrete subclasses to
/// communicate layout constraints between parents and children.
///
/// ## Writing a Constraints subclass
///
/// When creating a new [RenderObject] subclass with a new layout protocol, one
/// will usually need to create a new [Constraints] subclass to express the
/// input to the layout algorithms.
///
/// A [Constraints] subclass should be immutable (all fields final). There are
/// several members to implement, in addition to whatever fields, constructors,
/// and helper methods one may find useful for a particular layout protocol:
///
/// * The [isTight] getter, which should return true if the object represents a
/// case where the [RenderObject] class has no choice for how to lay itself
/// out. For example, [BoxConstraints] returns true for [isTight] when both
/// the minimum and maximum widths and the minimum and maximum heights are
/// equal.
///
/// * The [isNormalized] getter, which should return true if the object
/// represents its data in its canonical form. Sometimes, it is possible for
/// fields to be redundant with each other, such that several different
/// representations have the same implications. For example, a
/// [BoxConstraints] instance with its minimum width greater than its maximum
/// width is equivalent to one where the maximum width is set to that minimum
/// width (`2<w<1` is equivalent to `2<w<2`, since minimum constraints have
/// priority). This getter is used by the default implementation of
/// [debugAssertIsValid].
///
/// * The [debugAssertIsValid] method, which should assert if there's anything
/// wrong with the constraints object. (We use this approach rather than
/// asserting in constructors so that our constructors can be `const` and so
/// that it is possible to create invalid constraints temporarily while
/// building valid ones.) See the implementation of
/// [BoxConstraints.debugAssertIsValid] for an example of the detailed checks
/// that can be made.
///
/// * The [==] operator and the [hashCode] getter, so that constraints can be
/// compared for equality. If a render object is given constraints that are
/// equal, then the rendering library will avoid laying the object out again
/// if it is not dirty.
///
/// * The [toString] method, which should describe the constraints so that they
/// appear in a usefully readable form in the output of [debugDumpRenderTree].
@immutable
abstract class Constraints {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Constraints();
/// Whether there is exactly one size possible given these constraints.
bool get isTight;
/// Whether the constraint is expressed in a consistent manner.
bool get isNormalized;
/// Asserts that the constraints are valid.
///
/// This might involve checks more detailed than [isNormalized].
///
/// For example, the [BoxConstraints] subclass verifies that the constraints
/// are not [double.nan].
///
/// If the `isAppliedConstraint` argument is true, then even stricter rules
/// are enforced. This argument is set to true when checking constraints that
/// are about to be applied to a [RenderObject] during layout, as opposed to
/// constraints that may be further affected by other constraints. For
/// example, the asserts for verifying the validity of
/// [RenderConstrainedBox.additionalConstraints] do not set this argument, but
/// the asserts for verifying the argument passed to the [RenderObject.layout]
/// method do.
///
/// The `informationCollector` argument takes an optional callback which is
/// called when an exception is to be thrown. The collected information is
/// then included in the message after the error line.
///
/// Returns the same as [isNormalized] if asserts are disabled.
bool debugAssertIsValid({
bool isAppliedConstraint = false,
InformationCollector? informationCollector,
}) {
assert(isNormalized);
return isNormalized;
}
}
/// Signature for a function that is called for each [RenderObject].
///
/// Used by [RenderObject.visitChildren] and [RenderObject.visitChildrenForSemantics].
typedef RenderObjectVisitor = void Function(RenderObject child);
/// Signature for a function that is called during layout.
///
/// Used by [RenderObject.invokeLayoutCallback].
typedef LayoutCallback<T extends Constraints> = void Function(T constraints);
class _LocalSemanticsHandle implements SemanticsHandle {
_LocalSemanticsHandle._(PipelineOwner owner, this.listener) : _owner = owner {
assert(debugMaybeDispatchCreated('rendering', '_LocalSemanticsHandle', this));
if (listener != null) {
_owner.semanticsOwner!.addListener(listener!);
}
}
final PipelineOwner _owner;
/// The callback that will be notified when the semantics tree updates.
final VoidCallback? listener;
@override
void dispose() {
assert(debugMaybeDispatchDisposed(this));
if (listener != null) {
_owner.semanticsOwner!.removeListener(listener!);
}
_owner._didDisposeSemanticsHandle();
}
}
/// The pipeline owner manages the rendering pipeline.
///
/// The pipeline owner provides an interface for driving the rendering pipeline
/// and stores the state about which render objects have requested to be visited
/// in each stage of the pipeline. To flush the pipeline, call the following
/// functions in order:
///
/// 1. [flushLayout] updates any render objects that need to compute their
/// layout. During this phase, the size and position of each render
/// object is calculated. Render objects might dirty their painting or
/// compositing state during this phase.
/// 2. [flushCompositingBits] updates any render objects that have dirty
/// compositing bits. During this phase, each render object learns whether
/// any of its children require compositing. This information is used during
/// the painting phase when selecting how to implement visual effects such as
/// clipping. If a render object has a composited child, it needs to use a
/// [Layer] to create the clip in order for the clip to apply to the
/// composited child (which will be painted into its own [Layer]).
/// 3. [flushPaint] visits any render objects that need to paint. During this
/// phase, render objects get a chance to record painting commands into
/// [PictureLayer]s and construct other composited [Layer]s.
/// 4. Finally, if semantics are enabled, [flushSemantics] will compile the
/// semantics for the render objects. This semantic information is used by
/// assistive technology to improve the accessibility of the render tree.
///
/// The [RendererBinding] holds the pipeline owner for the render objects that
/// 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].
base class PipelineOwner with DiagnosticableTreeMixin {
/// Creates a pipeline owner.
///
/// Typically created by the binding (e.g., [RendererBinding]), but can be
/// created separately from the binding to drive off-screen render objects
/// through the rendering pipeline.
PipelineOwner({
this.onNeedVisualUpdate,
this.onSemanticsOwnerCreated,
this.onSemanticsUpdate,
this.onSemanticsOwnerDisposed,
}) {
assert(debugMaybeDispatchCreated('rendering', 'PipelineOwner', this));
}
/// Called when a render object associated with this 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.
///
/// 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.
///
/// Typical implementations will schedule the creation of the initial
/// semantics tree.
final VoidCallback? onSemanticsOwnerCreated;
/// Called whenever this pipeline owner's semantics owner emits a [SemanticsUpdate].
///
/// Typical implementations will delegate the [SemanticsUpdate] to a [FlutterView]
/// that can handle the [SemanticsUpdate].
final SemanticsUpdateCallback? onSemanticsUpdate;
/// Called whenever this pipeline owner disposes its semantics owner.
///
/// Typical implementations will tear down the semantics tree.
final VoidCallback? onSemanticsOwnerDisposed;
/// Calls [onNeedVisualUpdate] if [onNeedVisualUpdate] is not null.
///
/// Used to notify the pipeline owner that an associated render object wishes
/// to update its visual appearance.
void requestVisualUpdate() {
if (onNeedVisualUpdate != null) {
onNeedVisualUpdate!();
} else {
_manifold?.requestVisualUpdate();
}
}
/// The unique object managed by this pipeline that has no parent.
RenderObject? get rootNode => _rootNode;
RenderObject? _rootNode;
set rootNode(RenderObject? value) {
if (_rootNode == value) {
return;
}
_rootNode?.detach();
_rootNode = value;
_rootNode?.attach(this);
}
// Whether the current [flushLayout] call should pause to incorporate the
// [RenderObject]s in `_nodesNeedingLayout` into the current dirty list,
// before continuing to process dirty relayout boundaries.
//
// This flag is set to true when a [RenderObject.invokeLayoutCallback]
// returns, to avoid laying out dirty relayout boundaries in an incorrect
// order and causing them to be laid out more than once per frame. See
// layout_builder_mutations_test.dart for an example.
//
// The new dirty nodes are not immediately merged after a
// [RenderObject.invokeLayoutCallback] call because we may encounter multiple
// such calls while processing a single relayout boundary in [flushLayout].
// Batching new dirty nodes can reduce the number of merges [flushLayout]
// has to perform.
bool _shouldMergeDirtyNodes = false;
List<RenderObject> _nodesNeedingLayout = <RenderObject>[];
/// The [RenderObject]s representing relayout boundaries which need to be laid out
/// in the next [flushLayout] pass.
///
/// Relayout boundaries are added when they are marked for layout.
/// Subclasses of [PipelineOwner] may use them to invalidate caches or
/// otherwise make performance optimizations. Since nodes may be marked for
/// layout at any time, they are best checked during [flushLayout].
///
/// Relayout boundaries owned by child [PipelineOwner]s are not included here.
///
/// Boundaries appear in an arbitrary order, and may appear multiple times.
@protected
@nonVirtual
Iterable<RenderObject> get nodesNeedingLayout => _nodesNeedingLayout;
/// Whether this pipeline is currently in the layout phase.
///
/// Specifically, whether [flushLayout] is currently running.
///
/// Only valid when asserts are enabled; in release builds, this
/// always returns false.
bool get debugDoingLayout => _debugDoingLayout;
bool _debugDoingLayout = false;
bool _debugDoingChildLayout = false;
/// Update the layout information for all dirty render objects.
///
/// This function is one of the core stages of the rendering pipeline. Layout
/// information is cleaned prior to painting so that render objects will
/// appear on screen in their up-to-date locations.
///
/// See [RendererBinding] for an example of how this function is used.
void flushLayout() {
if (!kReleaseMode) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (debugEnhanceLayoutTimelineArguments) {
debugTimelineArguments = <String, String>{
'dirty count': '${_nodesNeedingLayout.length}',
'dirty list': '$_nodesNeedingLayout',
};
}
return true;
}());
FlutterTimeline.startSync(
'LAYOUT$_debugRootSuffixForTimelineEventNames',
arguments: debugTimelineArguments,
);
}
assert(() {
_debugDoingLayout = true;
return true;
}());
try {
while (_nodesNeedingLayout.isNotEmpty) {
assert(!_shouldMergeDirtyNodes);
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (var i = 0; i < dirtyNodes.length; i++) {
if (_shouldMergeDirtyNodes) {
_shouldMergeDirtyNodes = false;
if (_nodesNeedingLayout.isNotEmpty) {
_nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));
break;
}
}
final RenderObject node = dirtyNodes[i];
if (node._needsLayout && node.owner == this) {
node._layoutWithoutResize();
}
}
// No need to merge dirty nodes generated from processing the last
// 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) {
FlutterTimeline.finishSync();
}
}
}
// This flag is used to allow the kinds of mutations performed by GlobalKey
// reparenting while a LayoutBuilder is being rebuilt and in so doing tries to
// move a node from another LayoutBuilder subtree that hasn't been updated
// yet. To set this, call [_enableMutationsToDirtySubtrees], which is called
// by [RenderObject.invokeLayoutCallback].
bool _debugAllowMutationsToDirtySubtrees = false;
// See [RenderObject.invokeLayoutCallback].
void _enableMutationsToDirtySubtrees(VoidCallback callback) {
assert(_debugDoingLayout);
bool? oldState;
assert(() {
oldState = _debugAllowMutationsToDirtySubtrees;
_debugAllowMutationsToDirtySubtrees = true;
return true;
}());
try {
callback();
} finally {
_shouldMergeDirtyNodes = true;
assert(() {
_debugAllowMutationsToDirtySubtrees = oldState!;
return true;
}());
}
}
final List<RenderObject> _nodesNeedingCompositingBitsUpdate = <RenderObject>[];
/// Updates the [RenderObject.needsCompositing] bits.
///
/// Called as part of the rendering pipeline after [flushLayout] and before
/// [flushPaint].
void flushCompositingBits() {
if (!kReleaseMode) {
FlutterTimeline.startSync('UPDATING COMPOSITING BITS$_debugRootSuffixForTimelineEventNames');
}
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this) {
node._updateCompositingBits();
}
}
_nodesNeedingCompositingBitsUpdate.clear();
for (final PipelineOwner child in _children) {
child.flushCompositingBits();
}
assert(
_nodesNeedingCompositingBitsUpdate.isEmpty,
'Child PipelineOwners must not dirty nodes in their parent.',
);
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
}
List<RenderObject> _nodesNeedingPaint = <RenderObject>[];
/// The [RenderObject]s which need to be painted in the next [flushPaint] pass.
///
/// [RenderObject]s marked with [RenderObject.isRepaintBoundary] are added
/// when they are marked needing paint. Subclasses of [PipelineOwner] may use them
/// to invalidate caches or otherwise make performance optimizations.
/// Since nodes may be marked for layout at any time, they are best checked during
/// [flushPaint].
///
/// Marked children of child [PipelineOwner]s are not included here.
@protected
@nonVirtual
Iterable<RenderObject> get nodesNeedingPaint => _nodesNeedingPaint;
/// Whether this pipeline is currently in the paint phase.
///
/// Specifically, whether [flushPaint] is currently running.
///
/// Only valid when asserts are enabled. In release builds,
/// this always returns false.
bool get debugDoingPaint => _debugDoingPaint;
bool _debugDoingPaint = false;
/// Update the display lists for all render objects.
///
/// This function is one of the core stages of the rendering pipeline.
/// Painting occurs after layout and before the scene is recomposited so that
/// scene is composited with up-to-date display lists for every render object.
///
/// See [RendererBinding] for an example of how this function is used.
void flushPaint() {
if (!kReleaseMode) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (debugEnhancePaintTimelineArguments) {
debugTimelineArguments = <String, String>{
'dirty count': '${_nodesNeedingPaint.length}',
'dirty list': '$_nodesNeedingPaint',
};
}
return true;
}());
FlutterTimeline.startSync(
'PAINT$_debugRootSuffixForTimelineEventNames',
arguments: debugTimelineArguments,
);
}
try {
assert(() {
_debugDoingPaint = true;
return true;
}());
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
for (final node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layerHandle.layer != null);
if ((node._needsPaint || node._needsCompositedLayerUpdate) && node.owner == this) {
if (node._layerHandle.layer!.attached) {
assert(node.isRepaintBoundary);
if (node._needsPaint) {
PaintingContext.repaintCompositedChild(node);
} else {
PaintingContext.updateLayerProperties(node);
}
} else {
node._skippedPaintingOnLayer();
}
}
}
for (final PipelineOwner child in _children) {
child.flushPaint();
}
assert(
_nodesNeedingPaint.isEmpty,
'Child PipelineOwners must not dirty nodes in their parent.',
);
} finally {
assert(() {
_debugDoingPaint = false;
return true;
}());
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
}
}
/// The object that is managing semantics for this pipeline owner, if any.
///
/// 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.
SemanticsOwner? get semanticsOwner => _semanticsOwner;
SemanticsOwner? _semanticsOwner;
/// Deprecated.
///
/// Use [SemanticsBinding.debugOutstandingSemanticsHandles] instead. This
/// API is broken because an outstanding semantics handle on a given pipeline
/// owner doesn't mean that semantics are actually produced.
@Deprecated(
'Use SemanticsBinding.debugOutstandingSemanticsHandles instead. '
'This API is broken (see ensureSemantics). '
'This feature was deprecated after v3.22.0-23.0.pre.',
)
int get debugOutstandingSemanticsHandles => _outstandingSemanticsHandles;
int _outstandingSemanticsHandles = 0;
/// Deprecated.
///
/// Call [SemanticsBinding.ensureSemantics] instead and optionally add a
/// listener to [PipelineOwner.semanticsOwner]. This API is broken as calling
/// it does not guarantee that semantics are produced.
@Deprecated(
'Call SemanticsBinding.ensureSemantics instead and optionally add a listener to PipelineOwner.semanticsOwner. '
'This API is broken; it does not guarantee that semantics are actually produced. '
'This feature was deprecated after v3.22.0-23.0.pre.',
)
SemanticsHandle ensureSemantics({VoidCallback? listener}) {
_outstandingSemanticsHandles += 1;
_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;
_updateSemanticsOwner();
}
bool _debugDoingSemantics = false;
final Set<RenderObject> _nodesNeedingSemantics = <RenderObject>{};
/// Update the semantics for render objects marked as needing a semantics
/// update.
///
/// Initially, only the root node, as scheduled by
/// [RenderObject.scheduleInitialSemantics], needs a semantics update.
///
/// This function is one of the core stages of the rendering pipeline. The
/// semantics are compiled after painting and only after
/// [RenderObject.scheduleInitialSemantics] has been called.
///
/// See [RendererBinding] for an example of how this function is used.
// See [_RenderObjectSemantics]'s documentation for detailed explanations on
// what this method does.
void flushSemantics() {
if (_semanticsOwner == null) {
return;
}
if (!kReleaseMode) {
FlutterTimeline.startSync('SEMANTICS$_debugRootSuffixForTimelineEventNames');
}
assert(_semanticsOwner != null);
assert(() {
_debugDoingSemantics = true;
return true;
}());
try {
// This has to be top-to-down order since the geometries of a child and its
// subtree depends on ancestors' transforms and clips. If it updates child
// first, it may use dirty geometry in parent's semantics node to
// calculate the geometries in the subtree.
final List<RenderObject> nodesToProcess =
_nodesNeedingSemantics
.where((RenderObject object) => !object._needsLayout && object.owner == this)
.toList()
..sort((RenderObject a, RenderObject b) => a.depth - b.depth);
_nodesNeedingSemantics.clear();
if (!kReleaseMode) {
FlutterTimeline.startSync('Semantics.updateChildren');
}
for (final node in nodesToProcess) {
if (node._semantics.parentDataDirty) {
// This node is either blocked by a sibling
// (via SemanticsConfiguration.isBlockingSemanticsOfPreviouslyPaintedNodes)
// or is hidden by parent through visitChildrenForSemantics. Otherwise,
// the parent node would have updated this node's parent data and it
// would not be dirty.
//
// Updating the parent data now may create a gap of render object with
// dirty parent data when this branch later rejoin the rendering tree.
continue;
}
node._semantics.updateChildren();
}
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
final RenderObject? rootNode = this.rootNode;
assert(() {
assert(nodesToProcess.isEmpty || rootNode != null);
if (rootNode != null) {
_RenderObjectSemantics.debugCheckForParentData(rootNode);
}
return true;
}());
if (!kReleaseMode) {
FlutterTimeline.startSync('Semantics.ensureGeometry');
}
for (final node in nodesToProcess) {
if (node._semantics.parentDataDirty) {
// same as above.
continue;
}
node._semantics.ensureGeometry();
}
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
if (!kReleaseMode) {
FlutterTimeline.startSync('Semantics.ensureSemanticsNode');
}
for (final RenderObject node in nodesToProcess.reversed) {
if (node._semantics.parentDataDirty) {
// same as above.
continue;
}
node._semantics.ensureSemanticsNode();
}
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
_semanticsOwner!.sendSemanticsUpdate();
for (final PipelineOwner child in _children) {
child.flushSemantics();
}
assert(
_nodesNeedingSemantics.isEmpty,
'Child PipelineOwners must not dirty nodes in their parent.',
);
} finally {
assert(() {
_debugDoingSemantics = false;
return true;
}());
if (!kReleaseMode) {
FlutterTimeline.finishSync();
}
}
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[
for (final PipelineOwner child in _children) child.toDiagnosticsNode(),
];
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<RenderObject>('rootNode', rootNode, defaultValue: null));
}
// TREE MANAGEMENT
final Set<PipelineOwner> _children = <PipelineOwner>{};
PipelineManifold? _manifold;
PipelineOwner? _debugParent;
bool _debugSetParent(PipelineOwner child, PipelineOwner? parent) {
child._debugParent = parent;
return true;
}
String get _debugRootSuffixForTimelineEventNames => _debugParent == null ? ' (root)' : '';
/// 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;
// Not updating the semantics owner here to not disrupt any of its clients
// in case we get re-attached. If necessary, semantics owner will be updated
// in "attach", or disposed in "dispose", if not reattached.
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);
if (!kReleaseMode) {
_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);
if (!kReleaseMode) {
_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);
}
/// Release any resources held by this pipeline owner.
///
/// Prior to calling this method the pipeline owner must be removed from the
/// pipeline owner tree, i.e. it must have neither a parent nor any children
/// (see [dropChild]). It also must be [detach]ed from any [PipelineManifold].
///
/// The object is no longer usable after calling dispose.
void dispose() {
assert(_children.isEmpty);
assert(rootNode == null);
assert(_manifold == null);
assert(_debugParent == null);
assert(debugMaybeDispatchDisposed(this));
_semanticsOwner?.dispose();
_semanticsOwner = null;
_nodesNeedingLayout.clear();
_nodesNeedingCompositingBitsUpdate.clear();
_nodesNeedingPaint.clear();
_nodesNeedingSemantics.clear();
}
}
/// 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();
}
/// An object in the render tree.
///
/// The [RenderObject] class hierarchy is the core of the rendering
/// library's reason for being.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=zmbmrw07qBc}
///
/// [RenderObject]s have a [parent], and have a slot called [parentData] in
/// which the parent [RenderObject] can store child-specific data, for example,
/// the child position. The [RenderObject] class also implements the basic
/// layout and paint protocols.
///
/// The [RenderObject] class, however, does not define a child model (e.g.
/// whether a node has zero, one, or more children). It also doesn't define a
/// coordinate system (e.g. whether children are positioned in Cartesian
/// coordinates, in polar coordinates, etc) or a specific layout protocol (e.g.
/// whether the layout is width-in-height-out, or constraint-in-size-out, or
/// whether the parent sets the size and position of the child before or after
/// the child lays out, etc; or indeed whether the children are allowed to read
/// their parent's [parentData] slot).
///
/// The [RenderBox] subclass introduces the opinion that the layout
/// system uses Cartesian coordinates.
///
/// ## Lifecycle
///
/// A [RenderObject] must [dispose] when it is no longer needed. The creator
/// of the object is responsible for disposing of it. Typically, the creator is
/// a [RenderObjectElement], and that element will dispose the object it creates
/// when it is unmounted.
///
/// [RenderObject]s are responsible for cleaning up any expensive resources
/// they hold when [dispose] is called, such as [Picture] or [Image] objects.
/// This includes any [Layer]s that the render object has directly created. The
/// base implementation of dispose will nullify the [layer] property. Subclasses
/// must also nullify any other layer(s) it directly creates.
///
/// ## Writing a RenderObject subclass
///
/// In most cases, subclassing [RenderObject] itself is overkill, and
/// [RenderBox] would be a better starting point. However, if a render object
/// doesn't want to use a Cartesian coordinate system, then it should indeed
/// inherit from [RenderObject] directly. This allows it to define its own
/// layout protocol by using a new subclass of [Constraints] rather than using
/// [BoxConstraints], and by potentially using an entirely new set of objects
/// and values to represent the result of the output rather than just a [Size].
/// This increased flexibility comes at the cost of not being able to rely on
/// the features of [RenderBox]. For example, [RenderBox] implements an
/// intrinsic sizing protocol that allows you to measure a child without fully
/// laying it out, in such a way that if that child changes size, the parent
/// will be laid out again (to take into account the new dimensions of the
/// child). This is a subtle and bug-prone feature to get right.
///
/// Most aspects of writing a [RenderBox] apply to writing a [RenderObject] as
/// well, and therefore the discussion at [RenderBox] is recommended background
/// reading. The main differences are around layout and hit testing, since those
/// are the aspects that [RenderBox] primarily specializes.
///
/// ### Layout
///
/// A layout protocol begins with a subclass of [Constraints]. See the
/// discussion at [Constraints] for more information on how to write a
/// [Constraints] subclass.
///
/// The [performLayout] method should take the [constraints], and apply them.
/// The output of the layout algorithm is fields set on the object that describe
/// the geometry of the object for the purposes of the parent's layout. For
/// example, with [RenderBox] the output is the [RenderBox.size] field. This
/// output should only be read by the parent if the parent specified
/// `parentUsesSize` as true when calling [layout] on the child.
///
/// Anytime anything changes on a render object that would affect the layout of
/// that object, it should call [markNeedsLayout].
///
/// ### Hit Testing
///
/// Hit testing is even more open-ended than layout. There is no method to
/// override, you are expected to provide one.
///
/// The general behavior of your hit-testing method should be similar to the
/// behavior described for [RenderBox]. The main difference is that the input
/// need not be an [Offset]. You are also allowed to use a different subclass of
/// [HitTestEntry] when adding entries to the [HitTestResult]. When the
/// [handleEvent] method is called, the same object that was added to the
/// [HitTestResult] will be passed in, so it can be used to track information
/// like the precise coordinate of the hit, in whatever coordinate system is
/// used by the new layout protocol.
///
/// ### Adapting from one protocol to another
///
/// In general, the root of a Flutter render object tree is a [RenderView]. This
/// object has a single child, which must be a [RenderBox]. Thus, if you want to
/// have a custom [RenderObject] subclass in the render tree, you have two
/// choices: you either need to replace the [RenderView] itself, or you need to
/// have a [RenderBox] that has your class as its child. (The latter is the much
/// more common case.)
///
/// This [RenderBox] subclass converts from the box protocol to the protocol of
/// your class.
///
/// In particular, this means that for hit testing it overrides
/// [RenderBox.hitTest], and calls whatever method you have in your class for
/// hit testing.
///
/// Similarly, it overrides [performLayout] to create a [Constraints] object
/// appropriate for your class and passes that to the child's [layout] method.
///
/// ### Layout interactions between render objects
///
/// In general, the layout of a render object should only depend on the output of
/// its child's layout, and then only if `parentUsesSize` is set to true in the
/// [layout] call. Furthermore, if it is set to true, the parent must call the
/// child's [layout] if the child is to be rendered, because otherwise the
/// parent will not be notified when the child changes its layout outputs.
///
/// It is possible to set up render object protocols that transfer additional
/// information. For example, in the [RenderBox] protocol you can query your
/// children's intrinsic dimensions and baseline geometry. However, if this is
/// done then it is imperative that the child call [markNeedsLayout] on the
/// parent any time that additional information changes, if the parent used it
/// in the last layout phase. For an example of how to implement this, see the
/// [RenderBox.markNeedsLayout] method. It overrides
/// [RenderObject.markNeedsLayout] so that if a parent has queried the intrinsic
/// or baseline information, it gets marked dirty whenever the child's geometry
/// changes.
abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarget {
/// Initializes internal fields for subclasses.
RenderObject() {
assert(debugMaybeDispatchCreated('rendering', 'RenderObject', this));
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
_wasRepaintBoundary = isRepaintBoundary;
}
/// Cause the entire subtree rooted at the given [RenderObject] to be marked
/// dirty for layout, paint, etc, so that the effects of a hot reload can be
/// seen, or so that the effect of changing a global debug flag (such as
/// [debugPaintSizeEnabled]) can be applied.
///
/// This is called by the [RendererBinding] in response to the
/// `ext.flutter.reassemble` hook, which is used by development tools when the
/// application code has changed, to cause the widget tree to pick up any
/// changed implementations.
///
/// This is expensive and should not be called except during development.
///
/// See also:
///
/// * [BindingBase.reassembleApplication]
void reassemble() {
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsPaint();
markNeedsSemanticsUpdate();
visitChildren((RenderObject child) {
child.reassemble();
});
}
/// Whether this has been disposed.
///
/// If asserts are disabled, this property is always null.
bool? get debugDisposed {
bool? disposed;
assert(() {
disposed = _debugDisposed;
return true;
}());
return disposed;
}
bool _debugDisposed = false;
/// Release any resources held by this render object.
///
/// The object that creates a RenderObject is in charge of disposing it.
/// If this render object has created any children directly, it must dispose
/// of those children in this method as well. It must not dispose of any
/// children that were created by some other object, such as
/// a [RenderObjectElement]. Those children will be disposed when that
/// element unmounts, which may be delayed if the element is moved to another
/// part of the tree.
///
/// Implementations of this method must end with a call to the inherited
/// method, as in `super.dispose()`.
///
/// The object is no longer usable after calling dispose.
@mustCallSuper
void dispose() {
assert(!_debugDisposed);
assert(debugMaybeDispatchDisposed(this));
_layerHandle.layer = null;
assert(() {
// TODO(dnfield): Enable this assert once clients have had a chance to
// migrate.
// visitChildren((RenderObject child) {
// assert(
// child.debugDisposed!,
// '${child.runtimeType} (child of $runtimeType) must be disposed before calling super.dispose().',
// );
// });
_debugDisposed = true;
return true;
}());
}
// LAYOUT
/// Data for use by the parent render object.
///
/// The parent data is used by the render object that lays out this object
/// (typically this object's parent in the render tree) to store information
/// relevant to itself and to any other nodes who happen to know exactly what
/// the data means. The parent data is opaque to the child.
///
/// * The parent data field must not be directly set, except by calling
/// [setupParentData] on the parent node.
/// * The parent data can be set before the child is added to the parent, by
/// calling [setupParentData] on the future parent node.
/// * The conventions for using the parent data depend on the layout protocol
/// used between the parent and child. For example, in box layout, the
/// parent data is completely opaque but in sector layout the child is
/// permitted to read some fields of the parent data.
ParentData? parentData;
/// Override to setup parent data correctly for your children.
///
/// You can call this function to set up the parent data for child before the
/// child is added to the parent's child list.
void setupParentData(covariant RenderObject child) {
assert(_debugCanPerformMutations);
if (child.parentData is! ParentData) {
child.parentData = ParentData();
}
}
/// The depth of this render object in the render tree.
///
/// The depth of nodes in a tree monotonically increases as you traverse down
/// the tree: a node always has a [depth] greater than its ancestors.
/// There's no guarantee regarding depth between siblings.
///
/// The [depth] of a child can be more than one greater than the [depth] of
/// the parent, because the [depth] values are never decreased: all that
/// matters is that it's greater than the parent. Consider a tree with a root
/// node A, a child B, and a grandchild C. Initially, A will have [depth] 0,
/// B [depth] 1, and C [depth] 2. If C is moved to be a child of A,
/// sibling of B, then the numbers won't change. C's [depth] will still be 2.
///
/// The depth of a node is used to ensure that nodes are processed in
/// depth order. The [depth] is automatically maintained by the [adoptChild]
/// and [dropChild] methods.
int get depth => _depth;
int _depth = 0;
/// Adjust the [depth] of the given [child] to be greater than this node's own
/// [depth].
///
/// Only call this method from overrides of [redepthChildren].
@protected
void redepthChild(RenderObject child) {
assert(child.owner == owner);
if (child._depth <= _depth) {
child._depth = _depth + 1;
child.redepthChildren();
}
}
/// Adjust the [depth] of this node's children, if any.
///
/// Override this method in subclasses with child nodes to call [redepthChild]
/// for each child. Do not call this method directly.
@protected
void redepthChildren() {}
/// The parent of this render object in the render tree.
///
/// The [parent] of the root node in the render tree is null.
RenderObject? get parent => _parent;
RenderObject? _parent;
/// Called by subclasses when they decide a render object is a child.
///
/// Only for use by subclasses when changing their child lists. Calling this
/// in other cases will lead to an inconsistent tree and probably cause crashes.
@mustCallSuper
@protected
void adoptChild(RenderObject child) {
assert(child._parent == null);
assert(() {
var node = this;
while (node.parent != null) {
node = node.parent!;
}
assert(node != child); // indicates we are about to create a cycle
return true;
}());
setupParentData(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
child._parent = this;
if (attached) {
child.attach(_owner!);
}
redepthChild(child);
}
/// Called by subclasses when they decide a render object is no longer a child.
///
/// Only for use by subclasses when changing their child lists. Calling this
/// in other cases will lead to an inconsistent tree and probably cause crashes.
@mustCallSuper
@protected
void dropChild(RenderObject child) {
assert(child._parent == this);
assert(child.attached == attached);
assert(child.parentData != null);
if (!(child._isRelayoutBoundary ?? true)) {
child._isRelayoutBoundary = null;
}
child.parentData!.detach();
child.parentData = null;
child._parent = null;
if (attached) {
child.detach();
}
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
/// Calls visitor for each immediate child of this render object.
///
/// Override in subclasses with children and call the visitor for each child.
void visitChildren(RenderObjectVisitor visitor) {}
/// The object responsible for creating this render object.
///
/// Used in debug messages.
///
/// See also:
///
/// * [DebugCreator], which the [widgets] library uses as values for this field.
Object? debugCreator;
void _reportException(String method, Object exception, StackTrace stack) {
FlutterError.reportError(
FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'rendering library',
context: ErrorDescription('during $method()'),
informationCollector: () => <DiagnosticsNode>[
// debugCreator should always be null outside of debugMode, but we want
// the tree shaker to notice this.
if (kDebugMode && debugCreator != null) DiagnosticsDebugCreator(debugCreator!),
describeForError(
'The following RenderObject was being processed when the exception was fired',
),
// TODO(jacobr): this error message has a code smell. Consider whether
// displaying the truncated children is really useful for command line
// users. Inspector users can see the full tree by clicking on the
// render object so this may not be that useful.
describeForError('RenderObject', style: DiagnosticsTreeStyle.truncateChildren),
],
),
);
}
/// Whether [performResize] for this render object is currently running.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// false.
bool get debugDoingThisResize => _debugDoingThisResize;
bool _debugDoingThisResize = false;
/// Whether [performLayout] for this render object is currently running.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// false.
bool get debugDoingThisLayout => _debugDoingThisLayout;
bool _debugDoingThisLayout = false;
/// The render object that is actively computing layout.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// null.
static RenderObject? get debugActiveLayout => _debugActiveLayout;
static RenderObject? _debugActiveLayout;
/// Set [debugActiveLayout] to null when [inner] callback is called.
/// This is useful when you have to temporarily clear that variable to
/// disable some false-positive checks, such as when computing toStringDeep
/// or using custom trees.
@pragma('dart2js:tryInline')
@pragma('vm:prefer-inline')
@pragma('wasm:prefer-inline')
static T _withDebugActiveLayoutCleared<T>(T Function() inner) {
RenderObject? debugPreviousActiveLayout;
assert(() {
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = null;
return true;
}());
final T result = inner();
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
return true;
}());
return result;
}
/// Whether the parent render object is permitted to use this render object's
/// size.
///
/// Determined by the `parentUsesSize` parameter to [layout].
///
/// Only valid when asserts are enabled. In release builds, throws.
bool get debugCanParentUseSize => _debugCanParentUseSize!;
bool? _debugCanParentUseSize;
bool _debugMutationsLocked = false;
/// Whether tree mutations are currently permitted.
///
/// This is only useful during layout. One should also not mutate the tree at
/// other times (e.g. during paint or while assembling the semantic tree) but
/// this function does not currently enforce those conventions.
///
/// Only valid when asserts are enabled. This will throw in release builds.
bool get _debugCanPerformMutations {
late bool result;
assert(() {
if (_debugDisposed) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A disposed RenderObject was mutated.'),
DiagnosticsProperty<RenderObject>(
'The disposed RenderObject was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
final PipelineOwner? owner = this.owner;
// Detached nodes are allowed to mutate and the "can perform mutations"
// check will be performed when they re-attach. This assert is only useful
// during layout.
if (owner == null || !owner.debugDoingLayout) {
result = true;
return true;
}
RenderObject? activeLayoutRoot = this;
while (activeLayoutRoot != null) {
final bool mutationsToDirtySubtreesAllowed =
activeLayoutRoot.owner?._debugAllowMutationsToDirtySubtrees ?? false;
final bool doingLayoutWithCallback = activeLayoutRoot._doingThisLayoutWithCallback;
// Mutations on this subtree is allowed when:
// - the "activeLayoutRoot" subtree is being mutated in a layout callback.
// - a different part of the render tree is doing a layout callback,
// and this subtree is being reparented to that subtree, as a result
// of global key reparenting.
if (doingLayoutWithCallback ||
mutationsToDirtySubtreesAllowed && activeLayoutRoot._needsLayout) {
result = true;
return true;
}
if (!activeLayoutRoot._debugMutationsLocked) {
activeLayoutRoot = activeLayoutRoot.debugLayoutParent;
} else {
// activeLayoutRoot found.
break;
}
}
final RenderObject debugActiveLayout = RenderObject.debugActiveLayout!;
final culpritMethodName = debugActiveLayout.debugDoingThisLayout
? 'performLayout'
: 'performResize';
final culpritFullMethodName = '${debugActiveLayout.runtimeType}.$culpritMethodName';
result = false;
if (activeLayoutRoot == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A $runtimeType was mutated in $culpritFullMethodName.'),
ErrorDescription(
'The RenderObject was mutated when none of its ancestors is actively performing layout.',
),
DiagnosticsProperty<RenderObject>(
'The RenderObject being mutated was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
DiagnosticsProperty<RenderObject>(
'The RenderObject that was mutating the said $runtimeType was',
debugActiveLayout,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
if (activeLayoutRoot == this) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A $runtimeType was mutated in its own $culpritMethodName implementation.'),
ErrorDescription('A RenderObject must not re-dirty itself while still being laid out.'),
DiagnosticsProperty<RenderObject>(
'The RenderObject being mutated was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorHint(
'Consider using the LayoutBuilder widget to dynamically change a subtree during layout.',
),
]);
}
final summary = ErrorSummary('A $runtimeType was mutated in $culpritFullMethodName.');
final isMutatedByAncestor = activeLayoutRoot == debugActiveLayout;
final description = isMutatedByAncestor
? 'A RenderObject must not mutate its descendants in its $culpritMethodName method.'
: 'A RenderObject must not mutate another RenderObject from a different render subtree '
'in its $culpritMethodName method.';
throw FlutterError.fromParts(<DiagnosticsNode>[
summary,
ErrorDescription(description),
DiagnosticsProperty<RenderObject>(
'The RenderObject being mutated was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
DiagnosticsProperty<RenderObject>(
'The ${isMutatedByAncestor ? 'ancestor ' : ''}RenderObject that was mutating the said $runtimeType was',
debugActiveLayout,
style: DiagnosticsTreeStyle.errorProperty,
),
if (!isMutatedByAncestor)
DiagnosticsProperty<RenderObject>(
'Their common ancestor was',
activeLayoutRoot,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorHint(
'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. '
'Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.',
),
]);
}());
return result;
}
/// The [RenderObject] that's expected to call [layout] on this [RenderObject]
/// in its [performLayout] implementation.
///
/// This method is used to implement an assert that ensures the render subtree
/// actively performing layout can not get accidentally mutated. It's only
/// implemented in debug mode and always returns null in release mode.
///
/// The default implementation returns [parent] and overriding is rarely
/// needed. A [RenderObject] subclass that expects its
/// [RenderObject.performLayout] to be called from a different [RenderObject]
/// that's not its [parent] should override this property to return the actual
/// layout parent.
@protected
RenderObject? get debugLayoutParent {
RenderObject? layoutParent;
assert(() {
layoutParent = parent;
return true;
}());
return layoutParent;
}
/// The owner for this render object (null if unattached).
///
/// The entire render tree that this render object belongs to
/// will have the same owner.
PipelineOwner? get owner => _owner;
PipelineOwner? _owner;
/// Whether the render tree this render object belongs to is attached to a [PipelineOwner].
///
/// This becomes true during the call to [attach].
///
/// This becomes false during the call to [detach].
bool get attached => _owner != null;
/// Mark this render object as attached to the given owner.
///
/// Typically called only from the [parent]'s [attach] method, and by the
/// [owner] to mark the root of a tree as attached.
///
/// Subclasses with children should override this method to
/// [attach] all their children to the same [owner]
/// after calling the inherited method, as in `super.attach(owner)`.
@mustCallSuper
void attach(PipelineOwner owner) {
assert(!_debugDisposed);
assert(_owner == null);
_owner = owner;
// If the node was dirtied in some way while unattached, make sure to add
// it to the appropriate dirty list now that an owner is available
if (_needsLayout && _isRelayoutBoundary != null) {
// Don't enter this block if we've never laid out at all;
// scheduleInitialLayout() will handle it
_needsLayout = false;
markNeedsLayout();
}
if (_needsCompositingBitsUpdate) {
_needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate();
}
if (_needsPaint && _layerHandle.layer != null) {
// Don't enter this block if we've never painted at all;
// scheduleInitialPaint() will handle it
_needsPaint = false;
markNeedsPaint();
}
if (_semantics.configProvider.effective.isSemanticBoundary &&
(_semantics.parentDataDirty || !_semantics.built)) {
markNeedsSemanticsUpdate();
}
}
/// Mark this render object as detached from its [PipelineOwner].
///
/// Typically called only from the [parent]'s [detach], and by the [owner] to
/// mark the root of a tree as detached.
///
/// Subclasses with children should override this method to
/// [detach] all their children after calling the inherited method,
/// as in `super.detach()`.
@mustCallSuper
void detach() {
assert(_owner != null);
_owner = null;
assert(parent == null || attached == parent!.attached);
}
/// Whether this render object's layout information is dirty.
///
/// This is only set in debug mode. In general, render objects should not need
/// to condition their runtime behavior on whether they are dirty or not,
/// since they should only be marked dirty immediately prior to being laid
/// out and painted. In release builds, this throws.
///
/// It is intended to be used by tests and asserts.
bool get debugNeedsLayout {
late bool result;
assert(() {
result = _needsLayout;
return true;
}());
return result;
}
bool _needsLayout = true;
/// Whether this [RenderObject] is a known relayout boundary.
///
/// A relayout boundary is a [RenderObject] whose parent does not rely on the
/// child [RenderObject]'s size in its own layout algorithm. In other words,
/// if a [RenderObject]'s [performLayout] implementation does not ask the child
/// for its size at all, **the child** is a relayout boundary.
///
/// The type of "size" is typically defined by the coordinate system a
/// [RenderObject] subclass uses. For instance, [RenderSliver]s produce
/// [SliverGeometry] and [RenderBox]es produce [Size]. A parent [RenderObject]
/// may not read the child's size but still depend on the child's layout (using
/// a [RenderBox] child's baseline location, for example), this flag does not
/// reflect such dependencies and the [RenderObject] subclass must handle those
/// cases in its own implementation. See [RenderBox.markNeedsLayout] for an
/// example.
///
/// Relayout boundaries enable an important layout optimization: the parent not
/// depending on the size of a child means the child changing size does not
/// affect the layout of the parent. When a relayout boundary is marked as
/// needing layout, its parent does not have to be marked as dirty, hence the
/// name. For details, see [markNeedsLayout].
///
/// This flag is typically set in [RenderObject.layout], and consulted by
/// [markNeedsLayout] in deciding whether to recursively mark the parent as
/// also needing layout.
///
/// The flag is initially set to `null` when [layout] has yet been called, and
/// reset to `null` when the parent drops this child via [dropChild].
bool? _isRelayoutBoundary;
/// Whether [invokeLayoutCallback] for this render object is currently running.
bool get debugDoingThisLayoutWithCallback => _doingThisLayoutWithCallback;
bool _doingThisLayoutWithCallback = false;
/// The layout constraints most recently supplied by the parent.
///
/// If layout has not yet happened, accessing this getter will
/// throw a [StateError] exception.
@protected
Constraints get constraints {
if (_constraints == null) {
throw StateError('A RenderObject does not have any constraints before it has been laid out.');
}
return _constraints!;
}
Constraints? _constraints;
/// Verify that the object's constraints are being met. Override this function
/// in a subclass to verify that your state matches the constraints object.
/// This function is only called when asserts are enabled (i.e. in debug mode)
/// and only when needsLayout is false. If the constraints are not met, it
/// should assert or throw an exception.
@protected
void debugAssertDoesMeetConstraints();
/// When true, a debug method ([debugAssertDoesMeetConstraints], for instance)
/// is currently executing asserts for verifying the consistent behavior of
/// intrinsic dimensions methods.
///
/// This is typically set by framework debug methods. It is read by tests to
/// selectively ignore custom layout callbacks. It should not be set outside of
/// intrinsic-checking debug methods, and should not be checked in release mode
/// (where it will always be false).
static bool debugCheckingIntrinsics = false;
bool _debugRelayoutBoundaryAlreadyMarkedNeedsLayout() {
for (
RenderObject? node = this;
node != null && node._isRelayoutBoundary != null;
node = node.parent
) {
final bool alreadyMarkedNeedsLayout = node._needsLayout || node._debugDoingThisLayout;
if (!alreadyMarkedNeedsLayout) {
return false;
}
if (node._isRelayoutBoundary!) {
return true;
}
}
return true;
}
/// Mark this render object's layout information as dirty, and either register
/// this object with its [PipelineOwner], or defer to the parent, depending on
/// whether this object is a relayout boundary or not respectively.
///
/// ## Background
///
/// Rather than eagerly updating layout information in response to writes into
/// a render object, we instead mark the layout information as dirty, which
/// schedules a visual update. As part of the visual update, the rendering
/// pipeline updates the render object's layout information.
///
/// This mechanism batches the layout work so that multiple sequential writes
/// are coalesced, removing redundant computation.
///
/// If a render object's parent indicates that it uses the size of one of its
/// render object children when computing its layout information, this
/// function, when called for the child, will also mark the parent as needing
/// layout. In that case, since both the parent and the child need to have
/// their layout recomputed, the pipeline owner is only notified about the
/// parent; when the parent is laid out, it will call the child's [layout]
/// method and thus the child will be laid out as well.
///
/// Once [markNeedsLayout] has been called on a render object,
/// [debugNeedsLayout] returns true for that render object until just after
/// the pipeline owner has called [layout] on the render object.
///
/// ## Special cases
///
/// Some subclasses of [RenderObject], notably [RenderBox], have other
/// situations in which the parent needs to be notified if the child is
/// dirtied (e.g., if the child's intrinsic dimensions or baseline changes).
/// Such subclasses override markNeedsLayout and either call
/// `super.markNeedsLayout()`, in the normal case, or call
/// [markParentNeedsLayout], in the case where the parent needs to be laid out
/// as well as the child.
///
/// If [sizedByParent] has changed, calls
/// [markNeedsLayoutForSizedByParentChange] instead of [markNeedsLayout].
void markNeedsLayout() {
assert(_debugCanPerformMutations);
if (_needsLayout) {
assert(_debugRelayoutBoundaryAlreadyMarkedNeedsLayout());
return;
}
_needsLayout = true;
if (owner case final PipelineOwner owner? when (_isRelayoutBoundary ?? false)) {
assert(() {
if (debugPrintMarkNeedsLayoutStacks) {
debugPrintStack(label: 'markNeedsLayout() called for $this');
}
return true;
}());
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
} else if (parent != null) {
markParentNeedsLayout();
}
}
/// Mark this render object's layout information as dirty, and then defer to
/// the parent.
///
/// This function should only be called from [markNeedsLayout] or
/// [markNeedsLayoutForSizedByParentChange] implementations of subclasses that
/// introduce more reasons for deferring the handling of dirty layout to the
/// parent. See [markNeedsLayout] for details.
///
/// Only call this if [parent] is not null.
@protected
void markParentNeedsLayout() {
assert(_debugCanPerformMutations);
_needsLayout = true;
assert(this.parent != null);
final RenderObject parent = this.parent!;
if (!_doingThisLayoutWithCallback) {
parent.markNeedsLayout();
} else {
assert(parent._debugDoingThisLayout);
}
assert(parent == this.parent);
}
/// Mark this render object's layout information as dirty (like
/// [markNeedsLayout]), and additionally also handle any necessary work to
/// handle the case where [sizedByParent] has changed value.
///
/// This should be called whenever [sizedByParent] might have changed.
///
/// Only call this if [parent] is not null.
void markNeedsLayoutForSizedByParentChange() {
markNeedsLayout();
markParentNeedsLayout();
}
/// Bootstrap the rendering pipeline by scheduling the very first layout.
///
/// Requires this render object to be attached and that this render object
/// is the root of the render tree.
///
/// See [RenderView] for an example of how this function is used.
void scheduleInitialLayout() {
assert(!_debugDisposed);
assert(attached);
assert(parent == null);
assert(!owner!._debugDoingLayout);
assert(_isRelayoutBoundary == null);
_isRelayoutBoundary = true;
assert(() {
_debugCanParentUseSize = false;
return true;
}());
owner!._nodesNeedingLayout.add(this);
}
@pragma('vm:notify-debugger-on-exception')
void _layoutWithoutResize() {
assert(_needsLayout);
assert((_isRelayoutBoundary ?? false) || this is RenderObjectWithLayoutCallbackMixin);
RenderObject? debugPreviousActiveLayout;
assert(!_debugMutationsLocked);
assert(!_doingThisLayoutWithCallback);
assert(_debugCanParentUseSize != null);
assert(() {
_debugMutationsLocked = true;
_debugDoingThisLayout = true;
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
if (debugPrintLayouts) {
debugPrint('Laying out (without resize) $this');
}
return true;
}());
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_reportException('performLayout', e, stack);
}
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugMutationsLocked = false;
return true;
}());
_needsLayout = false;
markNeedsPaint();
}
/// Compute the layout for this render object.
///
/// This method is the main entry point for parents to ask their children to
/// update their layout information. The parent passes a constraints object,
/// which informs the child as to which layouts are permissible. The child is
/// required to obey the given constraints.
///
/// If the parent reads information computed during the child's layout, the
/// parent must pass true for `parentUsesSize`. In that case, the parent will
/// be marked as needing layout whenever the child is marked as needing layout
/// because the parent's layout information depends on the child's layout
/// information. If the parent uses the default value (false) for
/// `parentUsesSize`, the child can change its layout information (subject to
/// the given constraints) without informing the parent.
///
/// Subclasses should not override [layout] directly. Instead, they should
/// override [performResize] and/or [performLayout]. The [layout] method
/// delegates the actual work to [performResize] and [performLayout].
///
/// The parent's [performLayout] method should call the [layout] of all its
/// children unconditionally. It is the [layout] method's responsibility (as
/// implemented here) to return early if the child does not need to do any
/// work to update its layout information.
@pragma('vm:notify-debugger-on-exception')
void layout(Constraints constraints, {bool parentUsesSize = false}) {
assert(!_debugDisposed);
if (!kReleaseMode && debugProfileLayoutsEnabled) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (debugEnhanceLayoutTimelineArguments) {
debugTimelineArguments = toDiagnosticsNode().toTimelineArguments();
}
return true;
}());
FlutterTimeline.startSync('$runtimeType', arguments: debugTimelineArguments);
}
assert(
constraints.debugAssertIsValid(
isAppliedConstraint: true,
informationCollector: () {
final List<String> stack = StackTrace.current.toString().split('\n');
int? targetFrame;
final Pattern layoutFramePattern = RegExp(r'^#[0-9]+ +Render(?:Object|Box).layout \(');
for (var i = 0; i < stack.length; i += 1) {
if (layoutFramePattern.matchAsPrefix(stack[i]) != null) {
targetFrame = i + 1;
} else if (targetFrame != null) {
break;
}
}
if (targetFrame != null && targetFrame < stack.length) {
final Pattern targetFramePattern = RegExp(r'^#[0-9]+ +(.+)$');
final Match? targetFrameMatch = targetFramePattern.matchAsPrefix(stack[targetFrame]);
final String? problemFunction =
(targetFrameMatch != null && targetFrameMatch.groupCount > 0)
? targetFrameMatch.group(1)
: stack[targetFrame].trim();
return <DiagnosticsNode>[
ErrorDescription(
"These invalid constraints were provided to $runtimeType's layout() "
'function by the following function, which probably computed the '
'invalid constraints in question:\n'
' $problemFunction',
),
];
}
return <DiagnosticsNode>[];
},
),
);
assert(!_debugDoingThisResize);
assert(!_debugDoingThisLayout);
assert(() {
_debugCanParentUseSize = parentUsesSize;
return true;
}());
_isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent == null;
if (!_needsLayout && constraints == _constraints) {
assert(() {
// in case parentUsesSize changed since the last invocation, set size
// to itself, so it has the right internal debug values.
_debugDoingThisResize = sizedByParent;
_debugDoingThisLayout = !sizedByParent;
final RenderObject? debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
debugResetSize();
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugDoingThisResize = false;
return true;
}());
if (!kReleaseMode && debugProfileLayoutsEnabled) {
FlutterTimeline.finishSync();
}
return;
}
_constraints = constraints;
assert(!_debugMutationsLocked);
assert(!_doingThisLayoutWithCallback);
assert(() {
_debugMutationsLocked = true;
if (debugPrintLayouts) {
debugPrint(
'Laying out (${sizedByParent ? "with separate resize" : "with resize allowed"}) $this',
);
}
return true;
}());
if (sizedByParent) {
assert(() {
_debugDoingThisResize = true;
return true;
}());
try {
performResize();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_reportException('performResize', e, stack);
}
assert(() {
_debugDoingThisResize = false;
return true;
}());
}
RenderObject? debugPreviousActiveLayout;
assert(() {
_debugDoingThisLayout = true;
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
return true;
}());
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_reportException('performLayout', e, stack);
}
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugMutationsLocked = false;
return true;
}());
_needsLayout = false;
markNeedsPaint();
if (!kReleaseMode && debugProfileLayoutsEnabled) {
FlutterTimeline.finishSync();
}
}
/// If a subclass has a "size" (the state controlled by `parentUsesSize`,
/// whatever it is in the subclass, e.g. the actual `size` property of
/// [RenderBox]), and the subclass verifies that in debug mode this "size"
/// property isn't used when [debugCanParentUseSize] isn't set, then that
/// subclass should override [debugResetSize] to reapply the current values of
/// [debugCanParentUseSize] to that state.
@protected
void debugResetSize() {}
/// Whether the constraints are the only input to the sizing algorithm (in
/// particular, child nodes have no impact).
///
/// Returning false is always correct, but returning true can be more
/// efficient when computing the size of this render object because we don't
/// need to recompute the size if the constraints don't change.
///
/// Typically, subclasses will always return the same value. If the value can
/// change, then, when it does change, the subclass should make sure to call
/// [markNeedsLayoutForSizedByParentChange].
///
/// Subclasses that return true must not change the dimensions of this render
/// object in [performLayout]. Instead, that work should be done by
/// [performResize] or - for subclasses of [RenderBox] - in
/// [RenderBox.computeDryLayout].
@protected
bool get sizedByParent => false;
/// {@template flutter.rendering.RenderObject.performResize}
/// Updates the render objects size using only the constraints.
///
/// Do not call this function directly: call [layout] instead. This function
/// is called by [layout] when there is actually work to be done by this
/// render object during layout. The layout constraints provided by your
/// parent are available via the [constraints] getter.
///
/// This function is called only if [sizedByParent] is true.
/// {@endtemplate}
///
/// Subclasses that set [sizedByParent] to true should override this method to
/// compute their size. Subclasses of [RenderBox] should consider overriding
/// [RenderBox.computeDryLayout] instead.
@protected
void performResize();
/// Do the work of computing the layout for this render object.
///
/// Do not call this function directly: call [layout] instead. This function
/// is called by [layout] when there is actually work to be done by this
/// render object during layout. The layout constraints provided by your
/// parent are available via the [constraints] getter.
///
/// If [sizedByParent] is true, then this function should not actually change
/// the dimensions of this render object. Instead, that work should be done by
/// [performResize]. If [sizedByParent] is false, then this function should
/// both change the dimensions of this render object and instruct its children
/// to layout.
///
/// In implementing this function, you must call [layout] on each of your
/// children, passing true for parentUsesSize if your layout information is
/// dependent on your child's layout information. Passing true for
/// parentUsesSize ensures that this render object will undergo layout if the
/// child undergoes layout. Otherwise, the child can change its layout
/// information without informing this render object.
///
/// Some special [RenderObject] subclasses (such as the one used by
/// [OverlayPortal.overlayChildLayoutBuilder]) call [applyPaintTransform] in
/// their [performLayout] implementation. To ensure such [RenderObject]s get
/// the up-to-date paint transform, [RenderObject] subclasses should typically
/// update the paint transform (as reported by [applyPaintTransform]) in this
/// method instead of [paint].
@protected
void performLayout();
/// Allows mutations to be made to this object's child list (and any
/// descendants) as well as to any other dirty nodes in the render tree owned
/// by the same [PipelineOwner] as this object. The `callback` argument is
/// invoked synchronously, and the mutations are allowed only during that
/// callback's execution.
///
/// This exists to allow child lists to be built on-demand during layout (e.g.
/// based on the object's size), and to enable nodes to be moved around the
/// tree as this happens (e.g. to handle [GlobalKey] reparenting), while still
/// ensuring that any particular node is only laid out once per frame.
///
/// Calling this function disables a number of assertions that are intended to
/// catch likely bugs. As such, using this function is generally discouraged.
///
/// This function can only be called during layout.
@protected
void invokeLayoutCallback<T extends Constraints>(LayoutCallback<T> callback) {
assert(_debugMutationsLocked);
assert(_debugDoingThisLayout);
assert(!_doingThisLayoutWithCallback);
_doingThisLayoutWithCallback = true;
try {
owner!._enableMutationsToDirtySubtrees(() {
callback(constraints as T);
});
} finally {
_doingThisLayoutWithCallback = false;
}
}
// PAINTING
/// Whether [paint] for this render object is currently running.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// false.
bool get debugDoingThisPaint => _debugDoingThisPaint;
bool _debugDoingThisPaint = false;
/// The render object that is actively painting.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// null.
static RenderObject? get debugActivePaint => _debugActivePaint;
static RenderObject? _debugActivePaint;
/// Whether this render object repaints separately from its parent.
///
/// Override this in subclasses to indicate that instances of your class ought
/// to repaint independently. For example, render objects that repaint
/// frequently might want to repaint themselves without requiring their parent
/// to repaint.
///
/// If this getter returns true, the [paintBounds] are applied to this object
/// and all descendants. The framework invokes [RenderObject.updateCompositedLayer]
/// to create an [OffsetLayer] and assigns it to the [layer] field.
/// Render objects that declare themselves as repaint boundaries must not replace
/// the layer created by the framework.
///
/// If the value of this getter changes, [markNeedsCompositingBitsUpdate] must
/// be called.
///
/// See [RepaintBoundary] for more information about how repaint boundaries function.
bool get isRepaintBoundary => false;
/// Called, in debug mode, if [isRepaintBoundary] is true, when either the
/// this render object or its parent attempt to paint.
///
/// This can be used to record metrics about whether the node should actually
/// be a repaint boundary.
void debugRegisterRepaintBoundaryPaint({
bool includedParent = true,
bool includedChild = false,
}) {}
/// Whether this render object always needs compositing.
///
/// Override this in subclasses to indicate that your paint function always
/// creates at least one composited layer. For example, videos should return
/// true if they use hardware decoders.
///
/// You must call [markNeedsCompositingBitsUpdate] if the value of this getter
/// changes. (This is implied when [adoptChild] or [dropChild] are called.)
@protected
bool get alwaysNeedsCompositing => false;
late bool _wasRepaintBoundary;
/// Update the composited layer owned by this render object.
///
/// This method is called by the framework when [isRepaintBoundary] is true.
///
/// If [oldLayer] is `null`, this method must return a new [OffsetLayer]
/// (or subtype thereof). If [oldLayer] is not `null`, then this method must
/// reuse the layer instance that is provided - it is an error to create a new
/// layer in this instance. The layer will be disposed by the framework when
/// either the render object is disposed or if it is no longer a repaint
/// boundary.
///
/// The [OffsetLayer.offset] property will be managed by the framework and
/// must not be updated by this method.
///
/// If a property of the composited layer needs to be updated, the render object
/// must call [markNeedsCompositedLayerUpdate] which will schedule this method
/// to be called without repainting children. If this widget was marked as
/// needing to paint and needing a composited layer update, this method is only
/// called once.
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/102102 revisit the
// constraint that the instance/type of layer cannot be changed at runtime.
OffsetLayer updateCompositedLayer({required covariant OffsetLayer? oldLayer}) {
assert(isRepaintBoundary);
return oldLayer ?? OffsetLayer();
}
/// The compositing layer that this render object uses to repaint.
///
/// If this render object is not a repaint boundary, it is the responsibility
/// of the [paint] method to populate this field. If [needsCompositing] is
/// true, this field may be populated with the root-most layer used by the
/// render object implementation. When repainting, instead of creating a new
/// layer the render object may update the layer stored in this field for better
/// performance. It is also OK to leave this field as null and create a new
/// layer on every repaint, but without the performance benefit. If
/// [needsCompositing] is false, this field must be set to null either by
/// never populating this field, or by setting it to null when the value of
/// [needsCompositing] changes from true to false.
///
/// If a new layer is created and stored in some other field on the render
/// object, the render object must use a [LayerHandle] to store it. A layer
/// handle will prevent the layer from being disposed before the render
/// object is finished with it, and it will also make sure that the layer
/// gets appropriately disposed when the render object creates a replacement
/// or nulls it out. The render object must null out the [LayerHandle.layer]
/// in its [dispose] method.
///
/// If this render object is a repaint boundary, the framework automatically
/// creates an [OffsetLayer] and populates this field prior to calling the
/// [paint] method. The [paint] method must not replace the value of this
/// field.
@protected
ContainerLayer? get layer {
assert(!isRepaintBoundary || _layerHandle.layer == null || _layerHandle.layer is OffsetLayer);
return _layerHandle.layer;
}
@protected
set layer(ContainerLayer? newLayer) {
assert(
!isRepaintBoundary,
'Attempted to set a layer to a repaint boundary render object.\n'
'The framework creates and assigns an OffsetLayer to a repaint '
'boundary automatically.',
);
_layerHandle.layer = newLayer;
}
final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();
/// In debug mode, the compositing layer that this render object uses to repaint.
///
/// This getter is intended for debugging purposes only. In release builds, it
/// always returns null. In debug builds, it returns the layer even if the layer
/// is dirty.
///
/// For production code, consider [layer].
ContainerLayer? get debugLayer {
ContainerLayer? result;
assert(() {
result = _layerHandle.layer;
return true;
}());
return result;
}
bool _needsCompositingBitsUpdate = false; // set to true when a child is added
/// Mark the compositing state for this render object as dirty.
///
/// This is called to indicate that the value for [needsCompositing] needs to
/// be recomputed during the next [PipelineOwner.flushCompositingBits] engine
/// phase.
///
/// When the subtree is mutated, we need to recompute our
/// [needsCompositing] bit, and some of our ancestors need to do the
/// same (in case ours changed in a way that will change theirs). To
/// this end, [adoptChild] and [dropChild] call this method, and, as
/// necessary, this method calls the parent's, etc, walking up the
/// tree to mark all the nodes that need updating.
///
/// This method does not schedule a rendering frame, because since
/// it cannot be the case that _only_ the compositing bits changed,
/// something else will have scheduled a frame for us.
void markNeedsCompositingBitsUpdate() {
assert(!_debugDisposed);
if (_needsCompositingBitsUpdate) {
return;
}
_needsCompositingBitsUpdate = true;
final RenderObject? parent = this.parent;
if (parent != null) {
if (parent._needsCompositingBitsUpdate) {
return;
}
if ((!_wasRepaintBoundary || !isRepaintBoundary) && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
// parent is fine (or there isn't one), but we are dirty
owner?._nodesNeedingCompositingBitsUpdate.add(this);
}
late bool _needsCompositing; // initialized in the constructor
/// Whether we or one of our descendants has a compositing layer.
///
/// If this node needs compositing as indicated by this bit, then all ancestor
/// nodes will also need compositing.
///
/// Only legal to call after [PipelineOwner.flushLayout] and
/// [PipelineOwner.flushCompositingBits] have been called.
bool get needsCompositing {
assert(!_needsCompositingBitsUpdate); // make sure we don't use this bit when it is dirty
return _needsCompositing;
}
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate) {
return;
}
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing) {
_needsCompositing = true;
}
});
if (isRepaintBoundary || alwaysNeedsCompositing) {
_needsCompositing = true;
}
// If a node was previously a repaint boundary, but no longer is one, then
// regardless of its compositing state we need to find a new parent to
// paint from. To do this, we mark it clean again so that the traversal
// in markNeedsPaint is not short-circuited. It is removed from _nodesNeedingPaint
// so that we do not attempt to paint from it after locating a parent.
if (!isRepaintBoundary && _wasRepaintBoundary) {
_needsPaint = false;
_needsCompositedLayerUpdate = false;
owner?._nodesNeedingPaint.removeWhere((RenderObject t) => identical(t, this));
_needsCompositingBitsUpdate = false;
markNeedsPaint();
} else if (oldNeedsCompositing != _needsCompositing) {
_needsCompositingBitsUpdate = false;
markNeedsPaint();
} else {
_needsCompositingBitsUpdate = false;
}
}
/// Whether this render object's paint information is dirty.
///
/// This is only set in debug mode. In general, render objects should not need
/// to condition their runtime behavior on whether they are dirty or not,
/// since they should only be marked dirty immediately prior to being laid
/// out and painted. (In release builds, this throws.)
///
/// It is intended to be used by tests and asserts.
///
/// It is possible (and indeed, quite common) for [debugNeedsPaint] to be
/// false and [debugNeedsLayout] to be true. The render object will still be
/// repainted in the next frame when this is the case, because the
/// [markNeedsPaint] method is implicitly called by the framework after a
/// render object is laid out, prior to the paint phase.
bool get debugNeedsPaint {
late bool result;
assert(() {
result = _needsPaint;
return true;
}());
return result;
}
bool _needsPaint = true;
/// Whether this render object's layer information is dirty.
///
/// This is only set in debug mode. In general, render objects should not need
/// to condition their runtime behavior on whether they are dirty or not,
/// since they should only be marked dirty immediately prior to being laid
/// out and painted. (In release builds, this throws.)
///
/// It is intended to be used by tests and asserts.
bool get debugNeedsCompositedLayerUpdate {
late bool result;
assert(() {
result = _needsCompositedLayerUpdate;
return true;
}());
return result;
}
bool _needsCompositedLayerUpdate = false;
/// Mark this render object as having changed its visual appearance.
///
/// Rather than eagerly updating this render object's display list
/// in response to writes, we instead mark the render object as needing to
/// paint, which schedules a visual update. As part of the visual update, the
/// rendering pipeline will give this render object an opportunity to update
/// its display list.
///
/// This mechanism batches the painting work so that multiple sequential
/// writes are coalesced, removing redundant computation.
///
/// Once [markNeedsPaint] has been called on a render object,
/// [debugNeedsPaint] returns true for that render object until just after
/// the pipeline owner has called [paint] on the render object.
///
/// See also:
///
/// * [RepaintBoundary], to scope a subtree of render objects to their own
/// layer, thus limiting the number of nodes that [markNeedsPaint] must mark
/// dirty.
void markNeedsPaint() {
assert(!_debugDisposed);
assert(owner == null || !owner!.debugDoingPaint);
if (_needsPaint) {
return;
}
_needsPaint = true;
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if (isRepaintBoundary && _wasRepaintBoundary) {
assert(() {
if (debugPrintMarkNeedsPaintStacks) {
debugPrintStack(label: 'markNeedsPaint() called for $this');
}
return true;
}());
// If we always have our own layer, then we can just repaint
// ourselves without involving any other nodes.
assert(_layerHandle.layer is OffsetLayer);
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent != null) {
parent!.markNeedsPaint();
} else {
assert(() {
if (debugPrintMarkNeedsPaintStacks) {
debugPrintStack(label: 'markNeedsPaint() called for $this (root of render tree)');
}
return true;
}());
// If we are the root of the render tree and not a repaint boundary
// then we have to paint ourselves, since nobody else can paint us.
// We don't add ourselves to _nodesNeedingPaint in this case,
// because the root is always told to paint regardless.
//
// Trees rooted at a RenderView do not go through this
// code path because RenderViews are repaint boundaries.
owner?.requestVisualUpdate();
}
}
/// Mark this render object as having changed a property on its composited
/// layer.
///
/// Render objects that have a composited layer have [isRepaintBoundary] equal
/// to true may update the properties of that composited layer without repainting
/// their children. If this render object is a repaint boundary but does
/// not yet have a composited layer created for it, this method will instead
/// mark the nearest repaint boundary parent as needing to be painted.
///
/// If this method is called on a render object that is not a repaint boundary
/// or is a repaint boundary but hasn't been composited yet, it is equivalent
/// to calling [markNeedsPaint].
///
/// See also:
///
/// * [RenderOpacity], which uses this method when its opacity is updated to
/// update the layer opacity without repainting children.
void markNeedsCompositedLayerUpdate() {
assert(!_debugDisposed);
assert(owner == null || !owner!.debugDoingPaint);
if (_needsCompositedLayerUpdate || _needsPaint) {
return;
}
_needsCompositedLayerUpdate = true;
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if (isRepaintBoundary && _wasRepaintBoundary) {
// If we always have our own layer, then we can just repaint
// ourselves without involving any other nodes.
assert(_layerHandle.layer != null);
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else {
markNeedsPaint();
}
}
// Called when flushPaint() tries to make us paint but our layer is detached.
// To make sure that our subtree is repainted when it's finally reattached,
// even in the case where some ancestor layer is itself never marked dirty, we
// have to mark our entire detached subtree as dirty and needing to be
// repainted. That way, we'll eventually be repainted.
void _skippedPaintingOnLayer() {
assert(attached);
assert(isRepaintBoundary);
assert(_needsPaint || _needsCompositedLayerUpdate);
assert(_layerHandle.layer != null);
assert(!_layerHandle.layer!.attached);
RenderObject? node = parent;
while (node != null) {
if (node.isRepaintBoundary) {
if (node._layerHandle.layer == null) {
// Looks like the subtree here has never been painted. Let it handle itself.
break;
}
if (node._layerHandle.layer!.attached) {
// It's the one that detached us, so it's the one that will decide to repaint us.
break;
}
node._needsPaint = true;
}
node = node.parent;
}
}
/// Bootstrap the rendering pipeline by scheduling the very first paint.
///
/// Requires that this render object is attached, is the root of the render
/// tree, and has a composited layer.
///
/// See [RenderView] for an example of how this function is used.
void scheduleInitialPaint(ContainerLayer rootLayer) {
assert(rootLayer.attached);
assert(attached);
assert(parent == null);
assert(!owner!._debugDoingPaint);
assert(isRepaintBoundary);
assert(_layerHandle.layer == null);
_layerHandle.layer = rootLayer;
assert(_needsPaint);
owner!._nodesNeedingPaint.add(this);
}
/// Replace the layer. This is only valid for the root of a render
/// object subtree (whatever object [scheduleInitialPaint] was
/// called on).
///
/// This might be called if, e.g., the device pixel ratio changed.
void replaceRootLayer(OffsetLayer rootLayer) {
assert(!_debugDisposed);
assert(rootLayer.attached);
assert(attached);
assert(parent == null);
assert(!owner!._debugDoingPaint);
assert(isRepaintBoundary);
assert(_layerHandle.layer != null); // use scheduleInitialPaint the first time
_layerHandle.layer!.detach();
_layerHandle.layer = rootLayer;
markNeedsPaint();
}
void _paintWithContext(PaintingContext context, Offset offset) {
assert(!_debugDisposed);
assert(() {
if (_debugDoingThisPaint) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Tried to paint a RenderObject reentrantly.'),
describeForError(
'The following RenderObject was already being painted when it was '
'painted again',
),
ErrorDescription(
'Since this typically indicates an infinite recursion, it is '
'disallowed.',
),
]);
}
return true;
}());
// If we still need layout, then that means that we were skipped in the
// layout phase and therefore don't need painting. We might not know that
// yet (that is, our layer might not have been detached yet), because the
// same node that skipped us in layout is above us in the tree (obviously)
// and therefore may not have had a chance to paint yet (since the tree
// paints in reverse order). In particular this will happen if they have
// a different layer, because there's a repaint boundary between us.
if (_needsLayout) {
return;
}
if (!kReleaseMode && debugProfilePaintsEnabled) {
Map<String, String>? debugTimelineArguments;
assert(() {
if (debugEnhancePaintTimelineArguments) {
debugTimelineArguments = toDiagnosticsNode().toTimelineArguments();
}
return true;
}());
FlutterTimeline.startSync('$runtimeType', arguments: debugTimelineArguments);
}
assert(() {
if (_needsCompositingBitsUpdate) {
final RenderObject? parent = this.parent;
if (parent != null) {
var visitedByParent = false;
parent.visitChildren((RenderObject child) {
if (child == this) {
visitedByParent = true;
}
});
if (!visitedByParent) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
"A RenderObject was not visited by the parent's visitChildren "
'during paint.',
),
parent.describeForError('The parent was'),
describeForError('The child that was not visited was'),
ErrorDescription(
'A RenderObject with children must implement visitChildren and '
'call the visitor exactly once for each child; it also should not '
'paint children that were removed with dropChild.',
),
ErrorHint('This usually indicates an error in the Flutter framework itself.'),
]);
}
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Tried to paint a RenderObject before its compositing bits were '
'updated.',
),
describeForError(
'The following RenderObject was marked as having dirty compositing '
'bits at the time that it was painted',
),
ErrorDescription(
'A RenderObject that still has dirty compositing bits cannot be '
'painted because this indicates that the tree has not yet been '
'properly configured for creating the layer tree.',
),
ErrorHint('This usually indicates an error in the Flutter framework itself.'),
]);
}
return true;
}());
RenderObject? debugLastActivePaint;
assert(() {
_debugDoingThisPaint = true;
debugLastActivePaint = _debugActivePaint;
_debugActivePaint = this;
assert(!isRepaintBoundary || _layerHandle.layer != null);
return true;
}());
_needsPaint = false;
_needsCompositedLayerUpdate = false;
_wasRepaintBoundary = isRepaintBoundary;
try {
paint(context, offset);
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
} catch (e, stack) {
_reportException('paint', e, stack);
}
assert(() {
debugPaint(context, offset);
_debugActivePaint = debugLastActivePaint;
_debugDoingThisPaint = false;
return true;
}());
if (!kReleaseMode && debugProfilePaintsEnabled) {
FlutterTimeline.finishSync();
}
}
/// An estimate of the bounds within which this render object will paint.
/// Useful for debugging flags such as [debugPaintLayerBordersEnabled].
///
/// These are also the bounds used by [showOnScreen] to make a [RenderObject]
/// visible on screen.
Rect get paintBounds;
/// Override this method to paint debugging information.
void debugPaint(PaintingContext context, Offset offset) {}
/// Paint this render object into the given context at the given offset.
///
/// Subclasses should override this method to provide a visual appearance
/// for themselves. The render object's local coordinate system is
/// axis-aligned with the coordinate system of the context's canvas and the
/// render object's local origin (i.e, x=0 and y=0) is placed at the given
/// offset in the context's canvas.
///
/// Do not call this function directly. If you wish to paint yourself, call
/// [markNeedsPaint] instead to schedule a call to this function. If you wish
/// to paint one of your children, call [PaintingContext.paintChild] on the
/// given `context`.
///
/// When painting one of your children (via a paint child function on the
/// given context), the current canvas held by the context might change
/// because draw operations before and after painting children might need to
/// be recorded on separate compositing layers.
void paint(PaintingContext context, Offset offset) {}
/// Applies the transform that would be applied when painting the given child
/// to the given matrix.
///
/// Used by coordinate conversion functions ([getTransformTo], for example) to
/// translate coordinates local to one render object into coordinates local to
/// another render object.
///
/// Some RenderObjects will provide a zeroed out matrix in this method,
/// indicating that the child should not paint anything or respond to hit
/// tests currently. A parent may supply a non-zero matrix even though it
/// does not paint its child currently, for example if the parent is a
/// [RenderOffstage] with `offstage` set to true. In both of these cases,
/// the parent must return `false` from [paintsChild].
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
assert(child.parent == this);
}
/// Whether the given child would be painted if [paint] were called.
///
/// Some RenderObjects skip painting their children if they are configured to
/// not produce any visible effects. For example, a [RenderOffstage] with
/// its `offstage` property set to true, or a [RenderOpacity] with its opacity
/// value set to zero.
///
/// In these cases, the parent may still supply a non-zero matrix in
/// [applyPaintTransform] to inform callers about where it would paint the
/// child if the child were painted at all. Alternatively, the parent may
/// supply a zeroed out matrix if it would not otherwise be able to determine
/// a valid matrix for the child and thus cannot meaningfully determine where
/// the child would paint.
bool paintsChild(covariant RenderObject child) {
assert(child.parent == this);
return true;
}
/// {@template flutter.rendering.RenderObject.getTransformTo}
/// Applies the paint transform from this [RenderObject] to the `target`
/// [RenderObject].
///
/// Returns a matrix that maps the local paint coordinate system to the
/// coordinate system of `target`, or a [Matrix4.zero] if the paint transform
/// can not be computed.
///
/// This method throws an exception when the `target` is not in the same render
/// tree as this [RenderObject], as the behavior is undefined.
///
/// This method ignores [RenderObject.paintsChild]. This means it will still
/// try to compute the paint transform even if this [RenderObject] or
/// `target` is currently not visible.
///
/// If `target` is null, this method returns a matrix that maps from the
/// local paint coordinate system to the coordinate system of the
/// [PipelineOwner.rootNode].
/// {@endtemplate}
///
/// For the render tree owned by the [RendererBinding] (i.e. for the main
/// render tree displayed on the device) this means that this method maps to
/// the global coordinate system in logical pixels. To get physical pixels,
/// use [applyPaintTransform] from the [RenderView] to further transform the
/// coordinate.
Matrix4 getTransformTo(RenderObject? target) {
assert(attached);
// The paths from to fromRenderObject and toRenderObject's common ancestor.
// Each list's length is greater than 1 if not null.
//
// [this, ...., commonAncestorRenderObject], or null if `this` is the common
// ancestor.
List<RenderObject>? fromPath;
// [target, ...., commonAncestorRenderObject], or null if `target` is the
// common ancestor.
List<RenderObject>? toPath;
var from = this;
RenderObject to = target ?? owner!.rootNode!;
while (!identical(from, to)) {
final int fromDepth = from.depth;
final int toDepth = to.depth;
if (fromDepth >= toDepth) {
final RenderObject fromParent =
from.parent ??
(throw FlutterError('$target and $this are not in the same render tree.'));
(fromPath ??= <RenderObject>[this]).add(fromParent);
from = fromParent;
}
if (fromDepth <= toDepth) {
final RenderObject toParent =
to.parent ?? (throw FlutterError('$target and $this are not in the same render tree.'));
assert(
target != null,
'$this has a depth that is less than or equal to ${owner?.rootNode}',
);
(toPath ??= <RenderObject>[target!]).add(toParent);
to = toParent;
}
}
Matrix4? fromTransform;
if (fromPath != null) {
assert(fromPath.length > 1);
fromTransform = Matrix4.identity();
final int lastIndex = target == null ? fromPath.length - 2 : fromPath.length - 1;
for (var index = lastIndex; index > 0; index -= 1) {
fromPath[index].applyPaintTransform(fromPath[index - 1], fromTransform);
}
}
if (toPath == null) {
return fromTransform ?? Matrix4.identity();
}
assert(toPath.length > 1);
final toTransform = Matrix4.identity();
for (int index = toPath.length - 1; index > 0; index -= 1) {
toPath[index].applyPaintTransform(toPath[index - 1], toTransform);
}
if (toTransform.invert() == 0) {
// If the matrix is singular then `invert()` doesn't do anything.
return Matrix4.zero();
}
return (fromTransform?..multiply(toTransform)) ?? toTransform;
}
/// Returns a rect in this object's coordinate system that describes
/// the approximate bounding box of the clip rect that would be
/// applied to the given child during the paint phase, if any.
///
/// Returns null if the child would not be clipped.
///
/// This is used in the semantics phase to avoid including children
/// that are not physically visible.
///
/// RenderObjects that respect a [Clip] behavior when painting _must_ respect
/// that same behavior when describing this value. For example, if passing
/// [Clip.none] to [PaintingContext.pushClipRect] as the `clipBehavior`, then
/// the implementation of this method must return null.
Rect? describeApproximatePaintClip(covariant RenderObject child) => null;
/// Returns a rect in this object's coordinate system that describes
/// which [SemanticsNode]s produced by the `child` should be included in the
/// semantics tree. [SemanticsNode]s from the `child` that are positioned
/// outside of this rect will be dropped. Child [SemanticsNode]s that are
/// positioned inside this rect, but outside of [describeApproximatePaintClip]
/// will be included in the tree marked as hidden. Child [SemanticsNode]s
/// that are inside of both rect will be included in the tree as regular
/// nodes.
///
/// This method only returns a non-null value if the semantics clip rect
/// is different from the rect returned by [describeApproximatePaintClip].
/// If the semantics clip rect and the paint clip rect are the same, this
/// method returns null.
///
/// A viewport would typically implement this method to include semantic nodes
/// in the semantics tree that are currently hidden just before the leading
/// or just after the trailing edge. These nodes have to be included in the
/// semantics tree to implement implicit accessibility scrolling on iOS where
/// the viewport scrolls implicitly when moving the accessibility focus from
/// the last visible node in the viewport to the first hidden one.
///
/// See also:
///
/// * [RenderViewportBase.cacheExtent], used by viewports to extend their
/// semantics clip beyond their approximate paint clip.
Rect? describeSemanticsClip(covariant RenderObject? child) => null;
// SEMANTICS
/// Bootstrap the semantics reporting mechanism by marking this node
/// as needing a semantics update.
///
/// Requires that this render object is attached, and is the root of
/// the render tree.
///
/// See [RendererBinding] for an example of how this function is used.
void scheduleInitialSemantics() {
assert(!_debugDisposed);
assert(attached);
assert(parent == null);
assert(!owner!._debugDoingSemantics);
assert(_semantics.parentDataDirty || !_semantics.built);
assert(owner!._semanticsOwner != null);
owner!._nodesNeedingSemantics.add(this);
owner!.requestVisualUpdate();
}
/// Report the semantics of this node, for example for accessibility purposes.
///
/// This method should be overridden by subclasses that have interesting
/// semantic information.
///
/// The given [SemanticsConfiguration] object is mutable and should be
/// annotated in a manner that describes the current state. No reference
/// should be kept to that object; mutating it outside of the context of the
/// [describeSemanticsConfiguration] call (for example as a result of
/// asynchronous computation) will at best have no useful effect and at worse
/// will cause crashes as the data will be in an inconsistent state.
///
/// {@tool snippet}
///
/// The following snippet will describe the node as a button that responds to
/// tap actions.
///
/// ```dart
/// abstract class SemanticButtonRenderObject extends RenderObject {
/// @override
/// void describeSemanticsConfiguration(SemanticsConfiguration config) {
/// super.describeSemanticsConfiguration(config);
/// config
/// ..onTap = _handleTap
/// ..label = 'I am a button'
/// ..isButton = true;
/// }
///
/// void _handleTap() {
/// // Do something.
/// }
/// }
/// ```
/// {@end-tool}
@protected
void describeSemanticsConfiguration(SemanticsConfiguration config) {
// Nothing to do by default.
}
/// Sends a [SemanticsEvent] associated with this render object's [SemanticsNode].
///
/// If this render object has no semantics information, the first parent
/// render object with a non-null semantic node is used.
///
/// If semantics are disabled, no events are dispatched.
///
/// See [SemanticsNode.sendEvent] for a full description of the behavior.
void sendSemanticsEvent(SemanticsEvent semanticsEvent) {
if (owner!.semanticsOwner == null) {
return;
}
final SemanticsNode? node = _semantics.cachedSemanticsNode;
if (node != null && !node.isMergedIntoParent) {
node.sendEvent(semanticsEvent);
} else if (parent != null) {
parent!.sendSemanticsEvent(semanticsEvent);
}
}
/// The bounding box, in the local coordinate system, of this
/// object, for accessibility purposes.
Rect get semanticBounds;
/// Whether the semantics of this render object is dirty and await the update.
///
/// Always returns false in release mode.
bool get debugNeedsSemanticsUpdate {
if (kReleaseMode) {
return false;
}
return _semantics.parentDataDirty;
}
/// The semantics of this render object.
///
/// Exposed only for testing and debugging. To learn about the semantics of
/// render objects in production, obtain a [SemanticsHandle] from
/// [PipelineOwner.ensureSemantics].
///
/// Only valid in debug and profile mode. In release builds, always returns
/// null.
SemanticsNode? get debugSemantics {
// If _semantics.built is not true, the semantics node is an old cache and
// is not on the semantics tree.
if (!kReleaseMode && _semantics.built) {
return _semantics.cachedSemanticsNode;
}
return null;
}
/// Removes all semantics from this render object and its descendants.
///
/// Should only be called on objects whose [parent] is not a [RenderObject].
///
/// Override this method if you instantiate new [SemanticsNode]s in an
/// overridden [assembleSemanticsNode] method, to dispose of those nodes.
@mustCallSuper
void clearSemantics() {
_semantics.clear();
visitChildren((RenderObject child) {
child.clearSemantics();
});
}
/// Mark this node as needing an update to its semantics description.
///
/// This must be called whenever the semantics configuration of this
/// [RenderObject] as annotated by [describeSemanticsConfiguration] changes in
/// any way to update the semantics tree.
void markNeedsSemanticsUpdate() {
assert(!_debugDisposed);
assert(!attached || !owner!._debugDoingSemantics);
if (!attached || owner!._semanticsOwner == null) {
return;
}
_semantics.markNeedsUpdate();
}
late final _RenderObjectSemantics _semantics = _RenderObjectSemantics(this);
/// Called when collecting the semantics of this node.
///
/// The implementation has to return the children in paint order skipping all
/// children that are not semantically relevant (e.g. because they are
/// invisible).
///
/// The default implementation mirrors the behavior of
/// [visitChildren] (which is supposed to walk all the children).
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
visitChildren(visitor);
}
/// Assemble the [SemanticsNode] for this [RenderObject].
///
/// If [describeSemanticsConfiguration] sets
/// [SemanticsConfiguration.isSemanticBoundary] to true, this method is called
/// with the `node` created for this [RenderObject], the `config` to be
/// applied to that node and the `children` [SemanticsNode]s that descendants
/// of this RenderObject have generated.
///
/// By default, the method will annotate `node` with `config` and add the
/// `children` to it.
///
/// Subclasses can override this method to add additional [SemanticsNode]s
/// to the tree. If new [SemanticsNode]s are instantiated in this method
/// they must be disposed in [clearSemantics].
void assembleSemanticsNode(
SemanticsNode node,
SemanticsConfiguration config,
Iterable<SemanticsNode> children,
) {
assert(node == _semantics.cachedSemanticsNode);
// TODO(a14n): remove the following cast by updating type of parameter in either updateWith or assembleSemanticsNode
node.updateWith(config: config, childrenInInversePaintOrder: children as List<SemanticsNode>);
}
// EVENTS
/// Override this method to handle pointer events that hit this render object.
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {}
// HIT TESTING
// RenderObject subclasses are expected to have a method like the following
// (with the signature being whatever passes for coordinates for this
// particular class):
//
// bool hitTest(HitTestResult result, { required Offset position }) {
// // If the given position is not inside this node, then return false.
// // Otherwise:
// // For each child that intersects the position, in z-order starting from
// // the top, call hitTest() for that child, passing it /result/, and the
// // coordinates converted to the child's coordinate origin, and stop at
// // the first child that returns true.
// // Then, add yourself to /result/, and return true.
// }
//
// If you add yourself to /result/ and still return false, then that means you
// will see events but so will objects below you.
/// Returns a human understandable name.
@override
String toStringShort() {
String header = describeIdentity(this);
if (!kReleaseMode) {
if (_debugDisposed) {
header += ' DISPOSED';
return header;
}
var count = 0;
for (
RenderObject? node = this;
node != null && !(node._isRelayoutBoundary ?? false);
node = node.parent
) {
if (node._isRelayoutBoundary == null) {
count = -1;
break;
}
count += 1;
}
if (count > 0) {
header += ' relayoutBoundary=up$count';
}
if (_needsLayout) {
header += ' NEEDS-LAYOUT';
}
if (_needsPaint) {
header += ' NEEDS-PAINT';
}
if (_needsCompositingBitsUpdate) {
header += ' NEEDS-COMPOSITING-BITS-UPDATE';
}
if (!attached) {
header += ' DETACHED';
}
}
return header;
}
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) => toStringShort();
/// Returns a description of the tree rooted at this node.
/// If the prefix argument is provided, then every line in the output
/// will be prefixed by that string.
@override
String toStringDeep({
String prefixLineOne = '',
String? prefixOtherLines = '',
DiagnosticLevel minLevel = DiagnosticLevel.debug,
int wrapWidth = 65,
}) {
return _withDebugActiveLayoutCleared(
() => super.toStringDeep(
prefixLineOne: prefixLineOne,
prefixOtherLines: prefixOtherLines,
minLevel: minLevel,
wrapWidth: wrapWidth,
),
);
}
/// Returns a one-line detailed description of the render object.
/// This description is often somewhat long.
///
/// This includes the same information for this RenderObject as given by
/// [toStringDeep], but does not recurse to any children.
@override
String toStringShallow({String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) {
return _withDebugActiveLayoutCleared(
() => super.toStringShallow(joiner: joiner, minLevel: minLevel),
);
}
@protected
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
FlagProperty('needsCompositing', value: _needsCompositing, ifTrue: 'needs compositing'),
);
properties.add(
DiagnosticsProperty<Object?>(
'creator',
debugCreator,
defaultValue: null,
level: DiagnosticLevel.debug,
),
);
properties.add(
DiagnosticsProperty<ParentData>(
'parentData',
parentData,
tooltip: (_debugCanParentUseSize ?? false) ? 'can use size' : null,
missingIfNull: true,
),
);
properties.add(
DiagnosticsProperty<Constraints>('constraints', _constraints, missingIfNull: true),
);
// don't access it via the "layer" getter since that's only valid when we don't need paint
properties.add(
DiagnosticsProperty<ContainerLayer>('layer', _layerHandle.layer, defaultValue: null),
);
properties.add(
DiagnosticsProperty<SemanticsNode>('semantics node', debugSemantics, defaultValue: null),
);
properties.add(
FlagProperty(
'isBlockingSemanticsOfPreviouslyPaintedNodes',
value: _semantics.configProvider.effective.isBlockingSemanticsOfPreviouslyPaintedNodes,
ifTrue: 'blocks semantics of earlier render objects below the common boundary',
),
);
properties.add(
FlagProperty(
'isSemanticBoundary',
value: _semantics.configProvider.effective.isSemanticBoundary,
ifTrue: 'semantic boundary',
),
);
}
@override
List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[];
/// Attempt to make (a portion of) this or a descendant [RenderObject] visible
/// on screen.
///
/// If `descendant` is provided, that [RenderObject] is made visible. If
/// `descendant` is omitted, this [RenderObject] is made visible.
///
/// The optional `rect` parameter describes which area of that [RenderObject]
/// should be shown on screen. If `rect` is null, the entire
/// [RenderObject] (as defined by its [paintBounds]) will be revealed. The
/// `rect` parameter is interpreted relative to the coordinate system of
/// `descendant` if that argument is provided and relative to this
/// [RenderObject] otherwise.
///
/// The `duration` parameter can be set to a non-zero value to bring the
/// target object on screen in an animation defined by `curve`.
///
/// See also:
///
/// * [RenderViewportBase.showInViewport], which [RenderViewportBase] and
/// [SingleChildScrollView] delegate this method to.
void showOnScreen({
RenderObject? descendant,
Rect? rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
parent?.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
}
/// Adds a debug representation of a [RenderObject] optimized for including in
/// error messages.
///
/// The default [style] of [DiagnosticsTreeStyle.shallow] ensures that all of
/// the properties of the render object are included in the error output but
/// none of the children of the object are.
///
/// You should always include a RenderObject in an error message if it is the
/// [RenderObject] causing the failure or contract violation of the error.
DiagnosticsNode describeForError(
String name, {
DiagnosticsTreeStyle style = DiagnosticsTreeStyle.shallow,
}) {
return toDiagnosticsNode(name: name, style: style);
}
}
/// Generic mixin for render objects with one child.
///
/// Provides a child model for a render object subclass that has
/// a unique child, which is accessible via the [child] getter.
///
/// This mixin is typically used to implement render objects created
/// in a [SingleChildRenderObjectWidget].
mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject {
/// Checks whether the given render object has the correct [runtimeType] to be
/// a child of this render object.
///
/// Does nothing if assertions are disabled.
///
/// Always returns true.
bool debugValidateChild(RenderObject child) {
assert(() {
if (child is! ChildType) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.',
),
ErrorDescription(
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.',
),
ErrorSpacer(),
DiagnosticsProperty<Object?>(
'The $runtimeType that expected a $ChildType child was created by',
debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorSpacer(),
DiagnosticsProperty<Object?>(
'The ${child.runtimeType} that did not match the expected child type '
'was created by',
child.debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
return true;
}());
return true;
}
ChildType? _child;
/// The render object's unique child.
ChildType? get child => _child;
set child(ChildType? value) {
if (_child != null) {
dropChild(_child!);
}
_child = value;
if (_child != null) {
adoptChild(_child!);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_child?.attach(owner);
}
@override
void detach() {
super.detach();
_child?.detach();
}
@override
void redepthChildren() {
if (_child != null) {
redepthChild(_child!);
}
}
@override
void visitChildren(RenderObjectVisitor visitor) {
if (_child != null) {
visitor(_child!);
}
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
return child != null
? <DiagnosticsNode>[child!.toDiagnosticsNode(name: 'child')]
: <DiagnosticsNode>[];
}
}
/// A mixin for managing [RenderObject] with a [layoutCallback], which will be
/// invoked during this [RenderObject]'s layout process if scheduled using
/// [scheduleLayoutCallback].
///
/// A layout callback is typically a callback that mutates the [RenderObject]'s
/// render subtree during the [RenderObject]'s layout process. When an ancestor
/// [RenderObject] chooses to skip laying out this [RenderObject] in its
/// [performLayout] implementation (for example, for performance reasons, an
/// [Overlay] may skip laying out an offstage [OverlayEntry] while keeping it in
/// the tree), normally the [layoutCallback] will not be invoked because the
/// [layout] method will not be called. This can be undesirable when the
/// [layoutCallback] involves rebuilding dirty widgets (most notably, the
/// [LayoutBuilder] widget). Unlike render subtrees, typically all dirty widgets
/// (even off-screen ones) in a widget tree must be rebuilt. This mixin makes
/// sure once scheduled, the [layoutCallback] method will be invoked even if it's
/// skipped by an ancestor [RenderObject], unless this [RenderObject] has never
/// been laid out.
///
/// Subclasses must not invoke the layout callback directly. Instead, call
/// [runLayoutCallback] in the [performLayout] implementation.
///
/// See also:
///
/// * [LayoutBuilder] and [SliverLayoutBuilder], which use the mixin.
mixin RenderObjectWithLayoutCallbackMixin on RenderObject {
// The initial value of this flag must be set to true to prevent the layout
// callback from being scheduled when the subtree has never been laid out (in
// which case the `constraints` or any other layout information is unknown).
bool _needsRebuild = true;
/// The layout callback to be invoked during [performLayout].
///
/// This method should not be invoked directly. Instead, call
/// [runLayoutCallback] in the [performLayout] implementation. This callback
/// will be invoked using [invokeLayoutCallback].
@visibleForOverriding
void layoutCallback();
/// Invokes [layoutCallback] with [invokeLayoutCallback].
///
/// This method must be called in [performLayout], typically as early as
/// possible before any layout work is done, to avoid re-dirtying any child
/// [RenderObject]s.
@mustCallSuper
void runLayoutCallback() {
assert(debugDoingThisLayout);
invokeLayoutCallback((_) => layoutCallback());
_needsRebuild = false;
}
/// Informs the framework that the layout callback has been updated and must be
/// invoked again when this [RenderObject] is ready for layout, even when an
/// ancestor [RenderObject] chooses to skip laying out this render subtree.
@mustCallSuper
void scheduleLayoutCallback() {
if (_needsRebuild) {
assert(debugNeedsLayout);
return;
}
_needsRebuild = true;
// This ensures that the layout callback will be run even if an ancestor
// chooses to not lay out this subtree (for example, obstructed OverlayEntries
// with `maintainState` set to true), to maintain the widget tree integrity
// (making sure global keys are unique, for example).
owner?._nodesNeedingLayout.add(this);
// In an active tree, markNeedsLayout is needed to inform the layout boundary
// that its child size may change.
super.markNeedsLayout();
}
}
/// Parent data to support a doubly-linked list of children.
///
/// The children can be traversed using [nextSibling] or [previousSibling],
/// which can be called on the parent data of the render objects
/// obtained via [ContainerRenderObjectMixin.firstChild] or
/// [ContainerRenderObjectMixin.lastChild].
mixin ContainerParentDataMixin<ChildType extends RenderObject> on ParentData {
/// The previous sibling in the parent's child list.
ChildType? previousSibling;
/// The next sibling in the parent's child list.
ChildType? nextSibling;
/// Clear the sibling pointers.
@override
void detach() {
assert(
previousSibling == null,
'Pointers to siblings must be nulled before detaching ParentData.',
);
assert(nextSibling == null, 'Pointers to siblings must be nulled before detaching ParentData.');
super.detach();
}
}
/// Generic mixin for render objects with a list of children.
///
/// Provides a child model for a render object subclass that has a doubly-linked
/// list of children.
///
/// The [ChildType] specifies the type of the children (extending [RenderObject]),
/// e.g. [RenderBox].
///
/// [ParentDataType] stores parent container data on its child render objects.
/// It must extend [ContainerParentDataMixin], which provides the interface
/// for visiting children. This data is populated by
/// [RenderObject.setupParentData] implemented by the class using this mixin.
///
/// When using [RenderBox] as the child type, you will usually want to make use of
/// [RenderBoxContainerDefaultsMixin] and extend [ContainerBoxParentData] for the
/// parent data.
///
/// Moreover, this is a required mixin for render objects returned to [MultiChildRenderObjectWidget].
///
/// See also:
///
/// * [SlottedContainerRenderObjectMixin], which organizes its children
/// in different named slots.
mixin ContainerRenderObjectMixin<
ChildType extends RenderObject,
ParentDataType extends ContainerParentDataMixin<ChildType>
>
on RenderObject {
bool _debugUltimatePreviousSiblingOf(ChildType child, {ChildType? equals}) {
var childParentData = child.parentData! as ParentDataType;
while (childParentData.previousSibling != null) {
assert(childParentData.previousSibling != child);
child = childParentData.previousSibling!;
childParentData = child.parentData! as ParentDataType;
}
return child == equals;
}
bool _debugUltimateNextSiblingOf(ChildType child, {ChildType? equals}) {
var childParentData = child.parentData! as ParentDataType;
while (childParentData.nextSibling != null) {
assert(childParentData.nextSibling != child);
child = childParentData.nextSibling!;
childParentData = child.parentData! as ParentDataType;
}
return child == equals;
}
int _childCount = 0;
/// The number of children.
int get childCount => _childCount;
/// Checks whether the given render object has the correct [runtimeType] to be
/// a child of this render object.
///
/// Does nothing if assertions are disabled.
///
/// Always returns true.
bool debugValidateChild(RenderObject child) {
assert(() {
if (child is! ChildType) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A $runtimeType expected a child of type $ChildType but received a '
'child of type ${child.runtimeType}.',
),
ErrorDescription(
'RenderObjects expect specific types of children because they '
'coordinate with their children during layout and paint. For '
'example, a RenderSliver cannot be the child of a RenderBox because '
'a RenderSliver does not understand the RenderBox layout protocol.',
),
ErrorSpacer(),
DiagnosticsProperty<Object?>(
'The $runtimeType that expected a $ChildType child was created by',
debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
ErrorSpacer(),
DiagnosticsProperty<Object?>(
'The ${child.runtimeType} that did not match the expected child type '
'was created by',
child.debugCreator,
style: DiagnosticsTreeStyle.errorProperty,
),
]);
}
return true;
}());
return true;
}
ChildType? _firstChild;
ChildType? _lastChild;
void _insertIntoChildList(ChildType child, {ChildType? after}) {
final childParentData = child.parentData! as ParentDataType;
assert(childParentData.nextSibling == null);
assert(childParentData.previousSibling == null);
_childCount += 1;
assert(_childCount > 0);
if (after == null) {
// insert at the start (_firstChild)
childParentData.nextSibling = _firstChild;
if (_firstChild != null) {
final firstChildParentData = _firstChild!.parentData! as ParentDataType;
firstChildParentData.previousSibling = child;
}
_firstChild = child;
_lastChild ??= child;
} else {
assert(_firstChild != null);
assert(_lastChild != null);
assert(_debugUltimatePreviousSiblingOf(after, equals: _firstChild));
assert(_debugUltimateNextSiblingOf(after, equals: _lastChild));
final afterParentData = after.parentData! as ParentDataType;
if (afterParentData.nextSibling == null) {
// insert at the end (_lastChild); we'll end up with two or more children
assert(after == _lastChild);
childParentData.previousSibling = after;
afterParentData.nextSibling = child;
_lastChild = child;
} else {
// insert in the middle; we'll end up with three or more children
// set up links from child to siblings
childParentData.nextSibling = afterParentData.nextSibling;
childParentData.previousSibling = after;
// set up links from siblings to child
final childPreviousSiblingParentData =
childParentData.previousSibling!.parentData! as ParentDataType;
final childNextSiblingParentData =
childParentData.nextSibling!.parentData! as ParentDataType;
childPreviousSiblingParentData.nextSibling = child;
childNextSiblingParentData.previousSibling = child;
assert(afterParentData.nextSibling == child);
}
}
}
/// Insert child into this render object's child list after the given child.
///
/// If `after` is null, then this inserts the child at the start of the list,
/// and the child becomes the new [firstChild].
void insert(ChildType child, {ChildType? after}) {
assert(child != this, 'A RenderObject cannot be inserted into itself.');
assert(
after != this,
'A RenderObject cannot simultaneously be both the parent and the sibling of another RenderObject.',
);
assert(child != after, 'A RenderObject cannot be inserted after itself.');
assert(child != _firstChild);
assert(child != _lastChild);
adoptChild(child);
assert(
child.parentData is ParentDataType,
'A child of $runtimeType has parentData of type ${child.parentData.runtimeType}, '
'which does not conform to $ParentDataType. Class using ContainerRenderObjectMixin '
'should override setupParentData() to set parentData to type $ParentDataType.',
);
_insertIntoChildList(child, after: after);
}
/// Append child to the end of this render object's child list.
void add(ChildType child) {
insert(child, after: _lastChild);
}
/// Add all the children to the end of this render object's child list.
void addAll(List<ChildType>? children) {
children?.forEach(add);
}
void _removeFromChildList(ChildType child) {
final childParentData = child.parentData! as ParentDataType;
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild));
assert(_childCount >= 0);
if (childParentData.previousSibling == null) {
assert(_firstChild == child);
_firstChild = childParentData.nextSibling;
} else {
final childPreviousSiblingParentData =
childParentData.previousSibling!.parentData! as ParentDataType;
childPreviousSiblingParentData.nextSibling = childParentData.nextSibling;
}
if (childParentData.nextSibling == null) {
assert(_lastChild == child);
_lastChild = childParentData.previousSibling;
} else {
final childNextSiblingParentData = childParentData.nextSibling!.parentData! as ParentDataType;
childNextSiblingParentData.previousSibling = childParentData.previousSibling;
}
childParentData.previousSibling = null;
childParentData.nextSibling = null;
_childCount -= 1;
}
/// Remove this child from the child list.
///
/// Requires the child to be present in the child list.
void remove(ChildType child) {
_removeFromChildList(child);
dropChild(child);
}
/// Remove all their children from this render object's child list.
///
/// More efficient than removing them individually.
void removeAll() {
ChildType? child = _firstChild;
while (child != null) {
final childParentData = child.parentData! as ParentDataType;
final ChildType? next = childParentData.nextSibling;
childParentData.previousSibling = null;
childParentData.nextSibling = null;
dropChild(child);
child = next;
}
_firstChild = null;
_lastChild = null;
_childCount = 0;
}
/// Move the given `child` in the child list to be after another child.
///
/// More efficient than removing and re-adding the child. Requires the child
/// to already be in the child list at some position. Pass null for `after` to
/// move the child to the start of the child list.
void move(ChildType child, {ChildType? after}) {
assert(child != this);
assert(after != this);
assert(child != after);
assert(child.parent == this);
final childParentData = child.parentData! as ParentDataType;
if (childParentData.previousSibling == after) {
return;
}
_removeFromChildList(child);
_insertIntoChildList(child, after: after);
markNeedsLayout();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
ChildType? child = _firstChild;
while (child != null) {
child.attach(owner);
final childParentData = child.parentData! as ParentDataType;
child = childParentData.nextSibling;
}
}
@override
void detach() {
super.detach();
ChildType? child = _firstChild;
while (child != null) {
child.detach();
final childParentData = child.parentData! as ParentDataType;
child = childParentData.nextSibling;
}
}
@override
void redepthChildren() {
ChildType? child = _firstChild;
while (child != null) {
redepthChild(child);
final childParentData = child.parentData! as ParentDataType;
child = childParentData.nextSibling;
}
}
@override
void visitChildren(RenderObjectVisitor visitor) {
ChildType? child = _firstChild;
while (child != null) {
visitor(child);
final childParentData = child.parentData! as ParentDataType;
child = childParentData.nextSibling;
}
}
/// The first child in the child list.
ChildType? get firstChild => _firstChild;
/// The last child in the child list.
ChildType? get lastChild => _lastChild;
/// The previous child before the given child in the child list.
ChildType? childBefore(ChildType child) {
assert(child.parent == this);
final childParentData = child.parentData! as ParentDataType;
return childParentData.previousSibling;
}
/// The next child after the given child in the child list.
ChildType? childAfter(ChildType child) {
assert(child.parent == this);
final childParentData = child.parentData! as ParentDataType;
return childParentData.nextSibling;
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final children = <DiagnosticsNode>[];
if (firstChild != null) {
ChildType child = firstChild!;
var count = 1;
while (true) {
children.add(child.toDiagnosticsNode(name: 'child $count'));
if (child == lastChild) {
break;
}
count += 1;
final childParentData = child.parentData! as ParentDataType;
child = childParentData.nextSibling!;
}
}
return children;
}
}
/// Mixin for [RenderObject] that will call [systemFontsDidChange] whenever the
/// system fonts change.
///
/// System fonts can change when the OS installs or removes a font. Use this
/// mixin if the [RenderObject] uses [TextPainter] or [Paragraph] to correctly
/// update the text when it happens.
mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
/// A callback that is called when system fonts have changed.
///
/// The framework defers the invocation of the callback to the
/// [SchedulerPhase.transientCallbacks] phase to ensure that the
/// [RenderObject]'s text layout is still valid when user interactions are in
/// progress (which usually take place during the [SchedulerPhase.idle] phase).
///
/// By default, [markNeedsLayout] is called on the [RenderObject]
/// implementing this mixin.
///
/// Subclass should override this method to clear any extra cache that depend
/// on font-related metrics.
@protected
@mustCallSuper
void systemFontsDidChange() {
markNeedsLayout();
}
bool _hasPendingSystemFontsDidChangeCallBack = false;
void _scheduleSystemFontsUpdate() {
assert(
SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle,
'${objectRuntimeType(this, "RelayoutWhenSystemFontsChangeMixin")}._scheduleSystemFontsUpdate() '
'called during ${SchedulerBinding.instance.schedulerPhase}.',
);
if (_hasPendingSystemFontsDidChangeCallBack) {
return;
}
_hasPendingSystemFontsDidChangeCallBack = true;
SchedulerBinding.instance.scheduleFrameCallback((Duration timeStamp) {
assert(_hasPendingSystemFontsDidChangeCallBack);
_hasPendingSystemFontsDidChangeCallBack = false;
assert(
attached || (debugDisposed ?? true),
'$this is detached during ${SchedulerBinding.instance.schedulerPhase} but is not disposed.',
);
if (attached) {
systemFontsDidChange();
}
});
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
// If there's a pending callback that would imply this node was detached
// between the idle phase and the next transientCallbacks phase. The tree
// can not be mutated between those two phases so that should never happen.
assert(!_hasPendingSystemFontsDidChangeCallBack);
PaintingBinding.instance.systemFonts.addListener(_scheduleSystemFontsUpdate);
}
@override
void detach() {
assert(!_hasPendingSystemFontsDidChangeCallBack);
PaintingBinding.instance.systemFonts.removeListener(_scheduleSystemFontsUpdate);
super.detach();
}
}
/// A mixin for [RenderObject]s that want to annotate the [SemanticsNode]
/// for their subtree.
mixin SemanticsAnnotationsMixin on RenderObject {
/// Initializes the semantics annotations for this mixin.
// Parameters added to this method should be marked as required to ensure
// callers of the method provide a value.
void initSemanticsAnnotations({
required SemanticsProperties properties,
required bool container,
required bool explicitChildNodes,
required bool excludeSemantics,
required bool blockUserActions,
required Locale? localeForSubtree,
required TextDirection? textDirection,
}) {
_properties = properties;
_container = container;
_explicitChildNodes = explicitChildNodes;
_excludeSemantics = excludeSemantics;
_blockUserActions = blockUserActions;
_localeForSubtree = localeForSubtree;
_textDirection = textDirection;
_updateAttributedFields(_properties);
}
/// All of the [SemanticsProperties] for this [SemanticsAnnotationsMixin].
SemanticsProperties get properties => _properties;
late SemanticsProperties _properties;
set properties(SemanticsProperties value) {
if (_properties == value) {
return;
}
_properties = value;
_updateAttributedFields(_properties);
markNeedsSemanticsUpdate();
}
/// If 'container' is true, this [RenderObject] will introduce a new
/// node in the semantics tree. Otherwise, the semantics will be
/// merged with the semantics of any ancestors.
///
/// Whether descendants of this [RenderObject] can add their semantic information
/// to the [SemanticsNode] introduced by this configuration is controlled by
/// [explicitChildNodes].
bool get container => _container;
late bool _container;
set container(bool value) {
if (container == value) {
return;
}
_container = value;
markNeedsSemanticsUpdate();
}
/// Whether descendants of this [RenderObject] are allowed to add semantic
/// information to the [SemanticsNode] annotated by this widget.
///
/// When set to false descendants are allowed to annotate [SemanticsNode]s of
/// their parent with the semantic information they want to contribute to the
/// semantic tree.
/// When set to true the only way for descendants to contribute semantic
/// information to the semantic tree is to introduce new explicit
/// [SemanticsNode]s to the tree.
///
/// This setting is often used in combination with
/// [SemanticsConfiguration.isSemanticBoundary] to create semantic boundaries
/// that are either writable or not for children.
bool get explicitChildNodes => _explicitChildNodes;
late bool _explicitChildNodes;
set explicitChildNodes(bool value) {
if (_explicitChildNodes == value) {
return;
}
_explicitChildNodes = value;
markNeedsSemanticsUpdate();
}
/// Whether descendants of this [RenderObject] should have their semantic
/// information ignored.
///
/// When this flag is set to true, all child semantics nodes are ignored.
/// This can be used as a convenience for cases where a child is wrapped in
/// an [ExcludeSemantics] widget and then another [Semantics] widget.
bool get excludeSemantics => _excludeSemantics;
late bool _excludeSemantics;
set excludeSemantics(bool value) {
if (_excludeSemantics == value) {
return;
}
_excludeSemantics = value;
markNeedsSemanticsUpdate();
}
/// Whether to block user interactions for the semantics subtree.
///
/// Setting this true prevents user from activating pointer related
/// [SemanticsAction]s, such as [SemanticsAction.tap] or
/// [SemanticsAction.longPress].
bool get blockUserActions => _blockUserActions;
late bool _blockUserActions;
set blockUserActions(bool value) {
if (_blockUserActions == value) {
return;
}
_blockUserActions = value;
markNeedsSemanticsUpdate();
}
/// The [Locale] for the semantics subtree.
///
/// Setting this to null will inherit locale from ancestor semantics node.
Locale? get localeForSubtree => _localeForSubtree;
Locale? _localeForSubtree;
set localeForSubtree(Locale? value) {
if (_localeForSubtree == value) {
return;
}
_localeForSubtree = value;
markNeedsSemanticsUpdate();
}
void _updateAttributedFields(SemanticsProperties value) {
_attributedLabel = _effectiveAttributedLabel(value);
_attributedValue = _effectiveAttributedValue(value);
_attributedIncreasedValue = _effectiveAttributedIncreasedValue(value);
_attributedDecreasedValue = _effectiveAttributedDecreasedValue(value);
_attributedHint = _effectiveAttributedHint(value);
}
AttributedString? _effectiveAttributedLabel(SemanticsProperties value) {
return value.attributedLabel ?? (value.label == null ? null : AttributedString(value.label!));
}
AttributedString? _effectiveAttributedValue(SemanticsProperties value) {
return value.attributedValue ?? (value.value == null ? null : AttributedString(value.value!));
}
AttributedString? _effectiveAttributedIncreasedValue(SemanticsProperties value) {
return value.attributedIncreasedValue ??
(value.increasedValue == null ? null : AttributedString(value.increasedValue!));
}
AttributedString? _effectiveAttributedDecreasedValue(SemanticsProperties value) {
return properties.attributedDecreasedValue ??
(value.decreasedValue == null ? null : AttributedString(value.decreasedValue!));
}
AttributedString? _effectiveAttributedHint(SemanticsProperties value) {
return value.attributedHint ?? (value.hint == null ? null : AttributedString(value.hint!));
}
AttributedString? _attributedLabel;
AttributedString? _attributedValue;
AttributedString? _attributedIncreasedValue;
AttributedString? _attributedDecreasedValue;
AttributedString? _attributedHint;
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given
/// value.
///
/// This must not be null if [SemanticsProperties.attributedLabel],
/// [SemanticsProperties.attributedHint],
/// [SemanticsProperties.attributedValue],
/// [SemanticsProperties.attributedIncreasedValue], or
/// [SemanticsProperties.attributedDecreasedValue] are not null.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (textDirection == value) {
return;
}
_textDirection = value;
markNeedsSemanticsUpdate();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (excludeSemantics) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = container;
config.explicitChildNodes = explicitChildNodes;
config.isBlockingUserActions = blockUserActions;
config.localeForSubtree = localeForSubtree;
assert(
((_properties.scopesRoute ?? false) && explicitChildNodes) ||
!(_properties.scopesRoute ?? false),
'explicitChildNodes must be set to true if scopes route is true',
);
assert(
!((_properties.toggled ?? false) && (_properties.checked ?? false)),
'A semantics node cannot be toggled and checked at the same time',
);
if (_properties.enabled != null) {
config.isEnabled = _properties.enabled;
}
if (_properties.checked != null) {
config.isChecked = _properties.checked;
}
if (_properties.mixed != null) {
config.isCheckStateMixed = _properties.mixed;
}
if (_properties.toggled != null) {
config.isToggled = _properties.toggled;
}
if (_properties.selected != null) {
config.isSelected = _properties.selected!;
}
if (_properties.button != null) {
config.isButton = _properties.button!;
}
if (_properties.expanded != null) {
config.isExpanded = _properties.expanded;
}
if (_properties.link != null) {
config.isLink = _properties.link!;
}
if (_properties.linkUrl != null) {
config.linkUrl = _properties.linkUrl;
}
if (_properties.slider != null) {
config.isSlider = _properties.slider!;
}
if (_properties.keyboardKey != null) {
config.isKeyboardKey = _properties.keyboardKey!;
}
if (_properties.header != null) {
config.isHeader = _properties.header!;
}
if (_properties.headingLevel != null) {
config.headingLevel = _properties.headingLevel!;
}
if (_properties.textField != null) {
config.isTextField = _properties.textField!;
}
if (_properties.readOnly != null) {
config.isReadOnly = _properties.readOnly!;
}
if (_properties.focusable != null) {
config.isFocusable = _properties.focusable!;
}
if (_properties.focused != null) {
config.isFocused = _properties.focused;
}
if (_properties.accessiblityFocusBlockType != null) {
config.accessiblityFocusBlockType = _properties.accessiblityFocusBlockType!;
}
if (_properties.inMutuallyExclusiveGroup != null) {
config.isInMutuallyExclusiveGroup = _properties.inMutuallyExclusiveGroup!;
}
if (_properties.obscured != null) {
config.isObscured = _properties.obscured!;
}
if (_properties.multiline != null) {
config.isMultiline = _properties.multiline!;
}
if (_properties.hidden != null) {
config.isHidden = _properties.hidden!;
}
if (_properties.image != null) {
config.isImage = _properties.image!;
}
if (_properties.isRequired != null) {
config.isRequired = _properties.isRequired;
}
if (_properties.identifier != null) {
config.identifier = _properties.identifier!;
}
if (_properties.traversalParentIdentifier != null) {
config.traversalParentIdentifier = _properties.traversalParentIdentifier;
}
if (_properties.traversalChildIdentifier != null) {
config.traversalChildIdentifier = _properties.traversalChildIdentifier;
}
if (_attributedLabel != null) {
config.attributedLabel = _attributedLabel!;
}
if (_attributedValue != null) {
config.attributedValue = _attributedValue!;
}
if (_attributedIncreasedValue != null) {
config.attributedIncreasedValue = _attributedIncreasedValue!;
}
if (_attributedDecreasedValue != null) {
config.attributedDecreasedValue = _attributedDecreasedValue!;
}
if (_attributedHint != null) {
config.attributedHint = _attributedHint!;
}
if (_properties.tooltip != null) {
config.tooltip = _properties.tooltip!;
}
if (_properties.hintOverrides != null && _properties.hintOverrides!.isNotEmpty) {
config.hintOverrides = _properties.hintOverrides;
}
if (_properties.scopesRoute != null) {
config.scopesRoute = _properties.scopesRoute!;
}
if (_properties.namesRoute != null) {
config.namesRoute = _properties.namesRoute!;
}
if (_properties.liveRegion != null) {
config.liveRegion = _properties.liveRegion!;
}
if (_properties.maxValueLength != null) {
config.maxValueLength = _properties.maxValueLength;
}
if (_properties.currentValueLength != null) {
config.currentValueLength = _properties.currentValueLength;
}
if (textDirection != null) {
config.textDirection = textDirection;
}
if (_properties.sortKey != null) {
config.sortKey = _properties.sortKey;
}
if (_properties.tagForChildren != null) {
config.addTagForChildren(_properties.tagForChildren!);
}
if (properties.role != null) {
config.role = _properties.role!;
}
if (_properties.controlsNodes != null) {
config.controlsNodes = _properties.controlsNodes;
}
if (config.validationResult != _properties.validationResult) {
config.validationResult = _properties.validationResult;
}
if (_properties.inputType != null) {
config.inputType = _properties.inputType!;
}
// Registering _perform* as action handlers instead of the user provided
// ones to ensure that changing a user provided handler from a non-null to
// another non-null value doesn't require a semantics update.
if (_properties.onTap != null) {
config.onTap = _performTap;
}
if (_properties.onLongPress != null) {
config.onLongPress = _performLongPress;
}
if (_properties.onDismiss != null) {
config.onDismiss = _performDismiss;
}
if (_properties.onScrollLeft != null) {
config.onScrollLeft = _performScrollLeft;
}
if (_properties.onScrollRight != null) {
config.onScrollRight = _performScrollRight;
}
if (_properties.onScrollUp != null) {
config.onScrollUp = _performScrollUp;
}
if (_properties.onScrollDown != null) {
config.onScrollDown = _performScrollDown;
}
if (_properties.onIncrease != null) {
config.onIncrease = _performIncrease;
}
if (_properties.onDecrease != null) {
config.onDecrease = _performDecrease;
}
if (_properties.onCopy != null) {
config.onCopy = _performCopy;
}
if (_properties.onCut != null) {
config.onCut = _performCut;
}
if (_properties.onPaste != null) {
config.onPaste = _performPaste;
}
if (_properties.onMoveCursorForwardByCharacter != null) {
config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
}
if (_properties.onMoveCursorBackwardByCharacter != null) {
config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
}
if (_properties.onMoveCursorForwardByWord != null) {
config.onMoveCursorForwardByWord = _performMoveCursorForwardByWord;
}
if (_properties.onMoveCursorBackwardByWord != null) {
config.onMoveCursorBackwardByWord = _performMoveCursorBackwardByWord;
}
if (_properties.onSetSelection != null) {
config.onSetSelection = _performSetSelection;
}
if (_properties.onSetText != null) {
config.onSetText = _performSetText;
}
if (_properties.onDidGainAccessibilityFocus != null) {
config.onDidGainAccessibilityFocus = _performDidGainAccessibilityFocus;
}
if (_properties.onDidLoseAccessibilityFocus != null) {
config.onDidLoseAccessibilityFocus = _performDidLoseAccessibilityFocus;
}
if (_properties.onFocus != null) {
config.onFocus = _performFocus;
}
if (_properties.onExpand != null) {
config.onExpand = _performExpand;
}
if (_properties.onCollapse != null) {
config.onCollapse = _performCollapse;
}
if (_properties.customSemanticsActions != null) {
config.customSemanticsActions = _properties.customSemanticsActions!;
}
}
void _performTap() {
_properties.onTap?.call();
}
void _performLongPress() {
_properties.onLongPress?.call();
}
void _performDismiss() {
_properties.onDismiss?.call();
}
void _performScrollLeft() {
_properties.onScrollLeft?.call();
}
void _performScrollRight() {
_properties.onScrollRight?.call();
}
void _performScrollUp() {
_properties.onScrollUp?.call();
}
void _performScrollDown() {
_properties.onScrollDown?.call();
}
void _performIncrease() {
_properties.onIncrease?.call();
}
void _performDecrease() {
_properties.onDecrease?.call();
}
void _performCopy() {
_properties.onCopy?.call();
}
void _performCut() {
_properties.onCut?.call();
}
void _performPaste() {
_properties.onPaste?.call();
}
void _performMoveCursorForwardByCharacter(bool extendSelection) {
_properties.onMoveCursorForwardByCharacter?.call(extendSelection);
}
void _performMoveCursorBackwardByCharacter(bool extendSelection) {
_properties.onMoveCursorBackwardByCharacter?.call(extendSelection);
}
void _performMoveCursorForwardByWord(bool extendSelection) {
_properties.onMoveCursorForwardByWord?.call(extendSelection);
}
void _performMoveCursorBackwardByWord(bool extendSelection) {
_properties.onMoveCursorBackwardByWord?.call(extendSelection);
}
void _performSetSelection(TextSelection selection) {
_properties.onSetSelection?.call(selection);
}
void _performSetText(String text) {
_properties.onSetText?.call(text);
}
void _performDidGainAccessibilityFocus() {
_properties.onDidGainAccessibilityFocus?.call();
}
void _performDidLoseAccessibilityFocus() {
_properties.onDidLoseAccessibilityFocus?.call();
}
void _performFocus() {
_properties.onFocus?.call();
}
void _performExpand() {
_properties.onExpand?.call();
}
void _performCollapse() {
_properties.onCollapse?.call();
}
}
/// Properties of _RenderObjectSemantics that are imposed from parent.
@immutable
final class _SemanticsParentData {
const _SemanticsParentData({
required this.mergeIntoParent,
required this.blocksUserActions,
required this.explicitChildNodes,
required this.tagsForChildren,
required this.localeForChildren,
required this.accessiblityFocusBlockType,
});
/// Whether [SemanticsNode]s created from this render object semantics subtree
/// will be merged into parent.
///
/// This is imposed by render objects of parent [MergeSemantics]s.
final bool mergeIntoParent;
/// Whether [SemanticsNode]s created from this render object semantics subtree
/// ignores user action such as [SemanticsAction.tap] and its friends.
///
/// This is imposed by render objects of parent [IgnorePointer]s or
/// [AbsorbPointer]s.
final bool blocksUserActions;
/// The **Accessibility Focus Block Type** controls how accessibility focus is blocked.
///
/// * **none**: Accessibility focus is **not blocked**.
/// * **blockSubtree**: Blocks accessibility focus for the entire subtree.
/// * **blockNode**: Blocks accessibility focus for the **current node only**.
///
/// Only `blockSubtree` from a parent will be propagated down.
final AccessiblityFocusBlockType? accessiblityFocusBlockType;
/// Any immediate render object semantics that
/// [_RenderObjectSemantics.contributesToSemanticsTree] should forms a node
///
/// This is imposed by parent render objects that set
/// [SemanticsConfiguration.explicitChildNodes] to true.
final bool explicitChildNodes;
/// Tags for immediate render object semantics that
/// [_RenderObjectSemantics.shouldFormSemanticsNode] is true.
final Set<SemanticsTag>? tagsForChildren;
final Locale? localeForChildren;
@override
bool operator ==(Object other) {
return other is _SemanticsParentData &&
other.mergeIntoParent == mergeIntoParent &&
other.blocksUserActions == blocksUserActions &&
other.explicitChildNodes == explicitChildNodes &&
other.localeForChildren == localeForChildren &&
setEquals<SemanticsTag>(other.tagsForChildren, tagsForChildren);
}
@override
int get hashCode {
return Object.hash(
mergeIntoParent,
blocksUserActions,
explicitChildNodes,
localeForChildren,
Object.hashAllUnordered(tagsForChildren ?? const <SemanticsTag>{}),
);
}
}
/// A wrapper class that handles the life cycle of the [SemanticsConfiguration]
/// of a [RenderObject].
///
/// Typically, this class calls [RenderObject.describeSemanticsConfiguration] to
/// update the config and owner of this object does not directly mutate the
/// the config.
///
/// In some cases during [PipelineOwner.flushSemantics], the config has to be
/// mutated due to [_SemanticsParentData] update to propagate updated property
/// to semantics node. One should use [updateConfig] to update the config in this
/// case.
///
/// To access the config stored in this wrapper, uses
/// [_SemanticsConfigurationProvider.effective] to access the latest config.
/// Uses [_SemanticsConfigurationProvider.original] if one wants to access the
/// raw config without post mutations.
class _SemanticsConfigurationProvider {
_SemanticsConfigurationProvider(this._renderObject);
/// The owning rendering object for this object.
final RenderObject _renderObject;
bool _isEffectiveConfigWritable = false;
SemanticsConfiguration? _originalConfiguration;
SemanticsConfiguration? _effectiveConfiguration;
bool get wasSemanticsBoundary => _originalConfiguration?.isSemanticBoundary ?? false;
/// The latest config that reflect any change done through [updateConfig].
SemanticsConfiguration get effective {
return _effectiveConfiguration ?? original;
}
/// The original config without any change through [updateConfig].
///
/// This is typically use to recalculate certain properties when mutating
/// [effective] since [effective] may contain stale data from previous update.
/// An example is [SemanticsConfiguration.isBlockingUserActions]. Otherwise,
/// use [effective] instead.
SemanticsConfiguration get original {
if (_originalConfiguration == null) {
_effectiveConfiguration = _originalConfiguration = SemanticsConfiguration();
_renderObject.describeSemanticsConfiguration(_originalConfiguration!);
assert(
!_originalConfiguration!.explicitChildNodes ||
_originalConfiguration!.childConfigurationsDelegate == null,
'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.',
);
}
return _originalConfiguration!;
}
/// Mutates the config
///
/// This does not change the [original], and the change reflects in
/// [effective].
void updateConfig(ValueSetter<SemanticsConfiguration> callback) {
if (!_isEffectiveConfigWritable) {
_effectiveConfiguration = original.copy();
_isEffectiveConfigWritable = true;
}
callback(_effectiveConfiguration!);
}
/// Absorb a list of config into [effective].
void absorbAll(Iterable<SemanticsConfiguration> configs) {
updateConfig((SemanticsConfiguration config) {
configs.forEach(config.absorb);
});
}
/// Reset any post mutation to [effective].
void reset() {
_effectiveConfiguration = original;
_isEffectiveConfigWritable = false;
}
/// Remove every cache in this wrapper.
///
/// This cause the [RenderObject.describeSemanticsConfiguration] to be
/// re-evaluated next time [effective] or [original] is called.
void clear() {
_isEffectiveConfigWritable = false;
_effectiveConfiguration = null;
_originalConfiguration = null;
}
}
/// A convenient abstract interface used for constructing the
/// [_RenderObjectSemantics] tree.
///
/// The _SemanticsFragment can be an [_IncompleteSemanticsFragment] or a
/// [_RenderObjectSemantics]. This interface is used so that
/// [_RenderObjectSemantics] can handle semantics configuration merging without
/// knowing whether the child fragment is backed by a render object or an
/// incomplete semantics fragment.
abstract class _SemanticsFragment {
SemanticsConfiguration? get configToMergeUp;
_RenderObjectSemantics get owner;
bool mergesToSibling = false;
void markSiblingConfigurationConflict(bool conflict);
}
/// A fragment that is generated from
/// [SemanticsConfiguration.childConfigurationsDelegate]
///
/// A render object can choose to add additional semantics config to be merged
/// upward besides itself. These configs can be added through
/// [SemanticsConfiguration.childConfigurationsDelegate] and will form
/// `_IncompleteSemanticsFragment`s
///
/// See [RenderParagraph] for an example usage.
class _IncompleteSemanticsFragment extends _SemanticsFragment {
_IncompleteSemanticsFragment(this.configToMergeUp, this.owner);
@override
final SemanticsConfiguration configToMergeUp;
@override
final _RenderObjectSemantics owner;
@override
void markSiblingConfigurationConflict(bool conflict) {
assert(!conflict);
}
}
typedef _MergeUpAndSiblingMergeGroups = (
List<_SemanticsFragment> mergeUp,
List<List<_SemanticsFragment>> siblingMergeGroups,
);
/// A wrapper class for a [RenderObject] that provides semantics related
/// properties and compilations.
///
/// ## A high level summary
///
/// The [PipelineOwner.flushSemantics] calls the [updateChildren] to
/// build/update a tree of [_RenderObjectSemantics] by querying dirty
/// RenderObjects about their [SemanticsConfiguration] and updating the
/// _RenderObjectSemantics of the render objects according to these information.
/// While doing that, [updateChildren] also decide what
/// _RenderObjectSemantics will have their own SemanticsNode later on.
/// After that, [PipelineOwner.flushSemantics] calls [ensureGeometry] to
/// calculate the geometries for these _RenderObjectSemantics. Finally,
/// [ensureSemanticsNode] compiles these _RenderObjectSemantics into the actual
/// SemanticsNodes that form the semantics tree.
///
/// ## Steps Breakdown
///
/// The _RenderObjectSemantics tree is compiled in four phases. Phase 1 and 2
/// are done in [updateChildren], Phase 3 is done in [ensureGeometry], and phase
/// 4 is done in [ensureSemanticsNode].
///
/// ### Phase 1
///
/// Gather all the merge up _RenderObjectSemantics(s) by walking the rendering
/// object tree.
///
/// They are stored in [mergeUp] and [siblingMergeGroups] and should mimic
/// rendering object tree closely but only contain [_RenderObjectSemantics] that
/// contributes to semantics tree. i.e. where [contributesToSemanticsTree] is
/// true.
///
/// ### Phase 2
///
/// Merge all fragments from [mergeUp] and decide which [_RenderObjectSemantics]
/// should form a node, i.e. [shouldFormSemanticsNode] is true. Stores the
/// [_RenderObjectSemantics] that should form a node into [_children].
///
/// At this point, walking the [_children] forms a tree
/// that exactly resemble the resulting semantics node tree.
///
/// ### Phase 3
///
/// Walks the [_children] and calculate their
/// [_SemanticsGeometry] based on renderObject relationship.
///
/// ### Phase 4
///
/// Walks the [_children] and produce semantics node for
/// each [_RenderObjectSemantics] plus the sibling nodes.
///
/// Phase 2, 3, 4 each depends on previous step to finished updating the the
/// entire _RenderObjectSemantics tree. All three of them require separate tree
/// walk.
class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeMixin {
_RenderObjectSemantics(this.renderObject)
: configProvider = _SemanticsConfigurationProvider(renderObject);
/// The owning rendering object for this wrapper.
final RenderObject renderObject;
bool _hasSiblingConflict = false;
bool? _blocksPreviousSibling;
// TODO(chunhtai): Figure out what to do when incomplete fragments are asked
// to form a semantics node.
//
// If this is true, the [contributesToSemanticsTree] will also return true.
// This is a workaround so that the incomplete fragments will not be forced to
// form nodes if the parent has explicitChildNode = true.
bool _containsIncompleteFragment = false;
// Whether the semantics information in [cachedSemanticsNode] is up-to-date.
//
// When this flag is false, the cached semantics node must not be presented to
// the platform a11y system until it's updated or removed from the tree.
bool built = false;
/// The cached node created directly by this Object.
///
/// This cache is filled after the this object is compiled (usually by
/// calling [ensureSemanticsNode] on this object or ancestors) and forms a
/// semantics node.
///
/// Caching the semantics node ensures the id is consistent in the life time
/// of this object.
///
/// `_RenderSemanticsObject` only forms semantics node if
/// `shouldFormSemanticsNode` is true.
SemanticsNode? cachedSemanticsNode;
/// The semantics nodes produced by this render object.
///
/// This is filled after [ensureSemanticsNode] is called on this object or
/// ancestors when [shouldFormSemanticsNode] is true. In most cases, this only
/// contains one semantics node equals to [cachedSemanticsNode].
///
/// If there are [siblingMergeGroups], the nodes produced from the sibling
/// merge groups are also stored in this list.
final List<SemanticsNode> semanticsNodes = <SemanticsNode>[];
/// Fragments that will merge up to parent rendering object semantics.
final List<_SemanticsFragment> mergeUp = <_SemanticsFragment>[];
/// A list to store immediate child [_RenderObjectSemantics]s that will form
/// semantics nodes.
final List<_RenderObjectSemantics> _children = <_RenderObjectSemantics>[];
/// Merge groups that will form additional sibling nodes.
final List<List<_SemanticsFragment>> siblingMergeGroups = <List<_SemanticsFragment>>[];
final Map<SemanticsNode, List<_SemanticsFragment>> _producedSiblingNodesAndOwners =
<SemanticsNode, List<_SemanticsFragment>>{};
_SemanticsParentData? parentData;
_SemanticsGeometry? geometry;
final _SemanticsConfigurationProvider configProvider;
@override
_RenderObjectSemantics get owner => this;
bool get parentDataDirty {
if (isRoot) {
return false;
}
return parentData == null;
}
/// If this forms a semantics node, all of the properties in config are
/// used in creating the node. There is nothing to be merged up.
@override
SemanticsConfiguration? get configToMergeUp =>
shouldFormSemanticsNode ? null : configProvider.effective;
bool get contributesToSemanticsTree {
return configProvider.effective.hasBeenAnnotated ||
_containsIncompleteFragment ||
configProvider.effective.isSemanticBoundary ||
isRoot;
}
bool get isRoot => renderObject.parent == null;
bool get shouldFormSemanticsNode {
if (configProvider.effective.isSemanticBoundary) {
return true;
}
if (isRoot) {
return true;
}
if (!contributesToSemanticsTree) {
return false;
}
assert(
parentData != null,
'If there is no explicit flag that enforce semantics node, parent data '
'must be updated before determining whether this object will form a node',
);
return parentData!.explicitChildNodes || _hasSiblingConflict;
}
static void debugCheckForParentData(RenderObject root) {
void debugCheckParentDataNotDirty(_RenderObjectSemantics semantics) {
assert(!semantics.parentDataDirty);
semantics._getNonBlockedChildren().forEach(debugCheckParentDataNotDirty);
}
debugCheckParentDataNotDirty(root._semantics);
}
/// Whether this render object semantics will block other render object
/// semantics behind it in render object order from parent.
///
/// The [BlockSemantics] widget will cause this property to be true and hide
/// any sibling widget behind it from semantics.
bool get isBlockingPreviousSibling {
if (_blocksPreviousSibling != null) {
return _blocksPreviousSibling!;
}
_blocksPreviousSibling = configProvider.effective.isBlockingSemanticsOfPreviouslyPaintedNodes;
if (_blocksPreviousSibling!) {
return true;
}
if (configProvider.effective.isSemanticBoundary) {
return false;
}
renderObject.visitChildrenForSemantics((RenderObject child) {
final _RenderObjectSemantics childSemantics = child._semantics;
if (childSemantics.isBlockingPreviousSibling) {
_blocksPreviousSibling = true;
}
});
return _blocksPreviousSibling!;
}
static bool shouldDrop(SemanticsNode node) => node.isInvisible;
void markNeedsBuild() {
built = false;
if (!parentDataDirty && !shouldFormSemanticsNode) {
return;
}
for (final List<_SemanticsFragment> group in siblingMergeGroups) {
for (final _RenderObjectSemantics semantics in group.whereType<_RenderObjectSemantics>()) {
if (semantics.parentDataDirty) {
continue;
}
if (!semantics.shouldFormSemanticsNode) {
// This render object semantics will need to be merged into a sibling
// node.
semantics.markNeedsBuild();
}
}
}
}
/// Updates the [parentData] for the [_RenderObjectSemantics]s in the
/// rendering subtree and forms a [_RenderObjectSemantics] tree where children
/// are stored in [_children].
///
/// This method does the the phase 1 and 2 of the four phases documented on
/// [_RenderObjectSemantics].
///
/// Gather all the merge up _RenderObjectSemantics(s) by walking the rendering
/// object tree.
///
/// They are stored in [mergeUp] and [siblingMergeGroups] and should mimic
/// rendering object tree closely but only contain [_RenderObjectSemantics] that
/// contributes to semantics tree. i.e.
/// [contributesToSemanticsTree] is true.
///
/// Merge all fragments from [mergeUp] and decide which [_RenderObjectSemantics]
/// should form a node. i.e. [shouldFormSemanticsNode] is true. Stores the
/// [_RenderObjectSemantics] that should form a node with elevation adjustments
/// into [_children].
void updateChildren() {
assert(parentData != null || isRoot, 'parent data can only be null for root rendering object');
configProvider.reset();
final Set<SemanticsTag>? tagsForChildren = _getTagsForChildren();
final bool explicitChildNodesForChildren =
isRoot ||
configProvider.effective.explicitChildNodes ||
// ParentData's explicitChildNode only
// propagate to children if this node doesn't
// contribute to semantics tree
(!contributesToSemanticsTree && (parentData?.explicitChildNodes ?? true));
final bool blocksUserAction =
(parentData?.blocksUserActions ?? false) || configProvider.effective.isBlockingUserActions;
AccessiblityFocusBlockType accessiblityFocusBlockType;
if (parentData?.accessiblityFocusBlockType == AccessiblityFocusBlockType.blockSubtree) {
accessiblityFocusBlockType = AccessiblityFocusBlockType.blockSubtree;
} else {
accessiblityFocusBlockType = configProvider.effective.accessiblityFocusBlockType;
}
// localeForSubtree from the config overrides parentData's inherited locale.
final Locale? localeForChildren =
configProvider.effective.localeForSubtree ?? parentData?.localeForChildren;
siblingMergeGroups.clear();
mergeUp.clear();
final childParentData = _SemanticsParentData(
mergeIntoParent:
(parentData?.mergeIntoParent ?? false) ||
configProvider.effective.isMergingSemanticsOfDescendants,
blocksUserActions: blocksUserAction,
accessiblityFocusBlockType: accessiblityFocusBlockType,
localeForChildren: localeForChildren,
explicitChildNodes: explicitChildNodesForChildren,
tagsForChildren: tagsForChildren,
);
final _MergeUpAndSiblingMergeGroups result = _collectChildMergeUpAndSiblingGroup(
childParentData,
);
mergeUp.addAll(result.$1);
siblingMergeGroups.addAll(result.$2);
// Construct tree for nodes that will form semantics nodes.
_children.clear();
if (!contributesToSemanticsTree) {
return;
}
_marksConflictsInMergeGroup(mergeUp, isMergeUp: true);
siblingMergeGroups.forEach(_marksConflictsInMergeGroup);
final Iterable<SemanticsConfiguration> mergeUpConfigs = mergeUp
.map<SemanticsConfiguration?>((_SemanticsFragment fragment) => fragment.configToMergeUp)
.whereType<SemanticsConfiguration>();
configProvider.absorbAll(mergeUpConfigs);
// merge up fragments below this object will not be visible to parent
// because they are either absorbed or will form a semantics node.
mergeUp.clear();
mergeUp.add(this);
for (final _RenderObjectSemantics childSemantics
in result.$1.whereType<_RenderObjectSemantics>()) {
assert(childSemantics.contributesToSemanticsTree);
if (childSemantics.shouldFormSemanticsNode) {
_children.add(childSemantics);
} else {
_children.addAll(childSemantics._children);
siblingMergeGroups.addAll(childSemantics.siblingMergeGroups);
}
}
final Set<SemanticsTag>? tags = parentData?.tagsForChildren;
if (tags != null) {
assert(tags.isNotEmpty);
configProvider.updateConfig((SemanticsConfiguration config) {
tags.forEach(config.addTagForChildren);
});
}
if (accessiblityFocusBlockType != configProvider.effective.accessiblityFocusBlockType) {
configProvider.updateConfig((SemanticsConfiguration config) {
config.accessiblityFocusBlockType = accessiblityFocusBlockType;
});
}
if (blocksUserAction != configProvider.effective.isBlockingUserActions) {
configProvider.updateConfig((SemanticsConfiguration config) {
config.isBlockingUserActions = blocksUserAction;
});
}
if (localeForChildren != configProvider.effective.locale) {
configProvider.updateConfig((SemanticsConfiguration config) {
config.locale = localeForChildren;
});
}
}
List<_RenderObjectSemantics> _getNonBlockedChildren() {
final result = <_RenderObjectSemantics>[];
renderObject.visitChildrenForSemantics((RenderObject renderChild) {
if (renderChild._semantics.isBlockingPreviousSibling) {
result.clear();
}
result.add(renderChild._semantics);
});
return result;
}
Set<SemanticsTag>? _getTagsForChildren() {
if (contributesToSemanticsTree) {
return configProvider.original.tagsForChildren?.toSet();
}
Set<SemanticsTag>? result;
if (configProvider.original.tagsForChildren != null) {
result = configProvider.original.tagsForChildren!.toSet();
}
if (parentData?.tagsForChildren != null) {
if (result == null) {
result = parentData!.tagsForChildren;
} else {
result.addAll(parentData!.tagsForChildren!);
}
}
return result;
}
_MergeUpAndSiblingMergeGroups _collectChildMergeUpAndSiblingGroup(
_SemanticsParentData childParentData,
) {
final mergeUp = <_SemanticsFragment>[];
final siblingMergeGroups = <List<_SemanticsFragment>>[];
final childConfigurations = <SemanticsConfiguration>[];
final ChildSemanticsConfigurationsDelegate? childConfigurationsDelegate =
configProvider.effective.childConfigurationsDelegate;
final hasChildConfigurationsDelegate = childConfigurationsDelegate != null;
final configToFragment = <SemanticsConfiguration, _SemanticsFragment>{};
// It is possible the childConfigurationsDelegate may produce incomplete
// fragments. In this case, this render object semantics need to absorb all
// the mergeUp from children before present itself to the parent to avoid
// the parent forcing incomplete fragments to form a node. This is done by
// _containsIncompleteFragment which in turns flips the
// contributesToSemanticsTree.
//
// The problem is we won't know whether it will generate incomplete
// fragments until it runs, but we have to decide whether to propagate the
// parent's explicitChildNodes before we collect child fragments.
//
// Therefore, we have to make an assumption now to assume it will generate
// incomplete fragment and not propagate explicitChildNodes.
final bool needsToMakeIncompleteFragmentAssumption =
hasChildConfigurationsDelegate && childParentData.explicitChildNodes;
final _SemanticsParentData effectiveChildParentData;
if (needsToMakeIncompleteFragmentAssumption) {
effectiveChildParentData = _SemanticsParentData(
mergeIntoParent: childParentData.mergeIntoParent,
blocksUserActions: childParentData.blocksUserActions,
accessiblityFocusBlockType: childParentData.accessiblityFocusBlockType,
explicitChildNodes: false,
tagsForChildren: childParentData.tagsForChildren,
localeForChildren: childParentData.localeForChildren,
);
} else {
effectiveChildParentData = childParentData;
}
for (final _RenderObjectSemantics childSemantics in _getNonBlockedChildren()) {
assert(!childSemantics.renderObject._needsLayout);
childSemantics._didUpdateParentData(effectiveChildParentData);
for (final _SemanticsFragment fragment in childSemantics.mergeUp) {
if (hasChildConfigurationsDelegate && fragment.configToMergeUp != null) {
// This fragment need to go through delegate to determine whether it
// merge up or not.
childConfigurations.add(fragment.configToMergeUp!);
configToFragment[fragment.configToMergeUp!] = fragment;
} else {
mergeUp.add(fragment);
}
}
if (!childSemantics.contributesToSemanticsTree) {
// This child semantics needs to propagate sibling merge group to be
// compiled by parent that contributes to semantics tree.
siblingMergeGroups.addAll(childSemantics.siblingMergeGroups);
}
}
_containsIncompleteFragment = false;
assert(childConfigurationsDelegate != null || configToFragment.isEmpty);
if (hasChildConfigurationsDelegate) {
final ChildSemanticsConfigurationsResult result = childConfigurationsDelegate(
childConfigurations,
);
mergeUp.addAll(
result.mergeUp.map<_SemanticsFragment>((SemanticsConfiguration config) {
final _SemanticsFragment? fragment = configToFragment[config];
if (fragment != null) {
return fragment;
}
_containsIncompleteFragment = true;
return _IncompleteSemanticsFragment(config, this);
}),
);
for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
siblingMergeGroups.add(
group.map<_SemanticsFragment>((SemanticsConfiguration config) {
final _SemanticsFragment? fragment = configToFragment[config];
if (fragment != null) {
return fragment;
}
_containsIncompleteFragment = true;
return _IncompleteSemanticsFragment(config, this);
}).toList(),
);
}
}
if (!_containsIncompleteFragment && needsToMakeIncompleteFragmentAssumption) {
// Assumption was wrong, we have to re-update the child.
mergeUp.clear();
siblingMergeGroups.clear();
for (final _RenderObjectSemantics childSemantics in _getNonBlockedChildren()) {
assert(childParentData.explicitChildNodes);
childSemantics._didUpdateParentData(childParentData);
mergeUp.addAll(childSemantics.mergeUp);
if (!childSemantics.contributesToSemanticsTree) {
// This child semantics needs to propagate sibling merge group to be
// compiled by parent that contributes to semantics tree.
siblingMergeGroups.addAll(childSemantics.siblingMergeGroups);
}
}
}
return (mergeUp, siblingMergeGroups);
}
void _didUpdateParentData(_SemanticsParentData newParentData) {
if (parentData == newParentData) {
return;
}
// Parent data changes may result in node formation changes.
geometry = null;
markNeedsBuild();
parentData = newParentData;
updateChildren();
}
/// Makes whether this fragment has a sibling fragment with conflicting
/// [SemanticsConfiguration].
@override
void markSiblingConfigurationConflict(bool conflict) {
_hasSiblingConflict = conflict;
}
/// Updates the [geometry] for this [_RenderObjectSemantics]s and its subtree
/// in [_children].
///
/// This method does the the phase 3 of the four phases documented on
/// [_RenderObjectSemantics].
///
/// This method is short-circuited if the subtree geometry won't
/// be affect after the update. (e.g. the size doesn't change, or new clip
/// rect doesn't clip the content).
void ensureGeometry() {
if (isRoot) {
if (geometry?.rect != renderObject.semanticBounds) {
markNeedsBuild();
}
geometry = _SemanticsGeometry.root(renderObject.semanticBounds);
}
assert(geometry != null);
_updateChildGeometry();
}
void _updateChildGeometry() {
assert(geometry != null);
final _SemanticsGeometry parentGeometry = geometry!;
for (final _RenderObjectSemantics child in _children) {
final _SemanticsGeometry childGeometry = _SemanticsGeometry.computeChildGeometry(
parentPaintClipRect: parentGeometry.paintClipRect,
parentSemanticsClipRect: parentGeometry.semanticsClipRect,
parentTransform: null,
parent: this,
child: child,
);
child._updateGeometry(newGeometry: childGeometry);
}
for (final _RenderObjectSemantics explicitSiblingChild
in siblingMergeGroups
.expand<_SemanticsFragment>((List<_SemanticsFragment> group) => group)
.whereType<_RenderObjectSemantics>()
.expand(
(_RenderObjectSemantics siblingChild) => siblingChild.shouldFormSemanticsNode
? <_RenderObjectSemantics>[siblingChild]
: siblingChild._children,
)) {
final _SemanticsGeometry childGeometry = _SemanticsGeometry.computeChildGeometry(
parentPaintClipRect: parentGeometry.paintClipRect,
parentSemanticsClipRect: parentGeometry.semanticsClipRect,
parentTransform: parentGeometry.transform,
parent: this,
child: explicitSiblingChild,
);
explicitSiblingChild._updateGeometry(newGeometry: childGeometry);
}
}
void _updateGeometry({required _SemanticsGeometry newGeometry}) {
geometry = newGeometry;
markNeedsBuild();
_updateChildGeometry();
}
/// Ensures the semantics nodes from this render object semantics subtree are
/// generated and up to date.
///
/// This method does the the phase 4 of the four phases documented on
/// [_RenderObjectSemantics].
///
/// This can only be called if the owning rendering object is a semantics
/// boundary. For non boundary rendering objects, they require semantics
/// information from both their parent and child rendering objects to update
/// its cache, so it can't update by themselves.
void ensureSemanticsNode() {
assert(configProvider.effective.isSemanticBoundary || isRoot);
if (!built) {
_buildSemantics(usedSemanticsIds: <int>{});
} else {
assert(built);
// parent data and parent geometry didn't change, there isn't anything to
// update for semantics nodes generated in this render object semantics.
//
// Therefore, we only need to update the subtree.
_buildSemanticsSubtree(usedSemanticsIds: <int>{});
}
}
/// Builds the semantics node and its semantics node subtree.
///
/// This method will in turn call [_buildSemanticsSubtree].
///
/// This method will short-circuit itself if [cachedSemanticsNode] is
/// already up-to-date.
void _buildSemantics({required Set<int> usedSemanticsIds}) {
assert(shouldFormSemanticsNode);
if (cachedSemanticsNode != null) {
// Any node other than producedNode in _semanticsNodes are sibling nodes
// from children fragments. This fragment is responsible for updating
// tags as well as cleaning up.
//
// Clean up the properties now so that we don't have stale data in them
// after the _produceSemanticsNode.
for (final SemanticsNode node in semanticsNodes) {
if (node != cachedSemanticsNode) {
node.tags = null;
}
}
}
if (!built) {
semanticsNodes.clear();
_producedSiblingNodesAndOwners.clear();
_produceSemanticsNode(usedSemanticsIds: usedSemanticsIds);
}
assert(built);
// Any node other than producedNode in _semanticsNodes are sibling nodes
// from children fragments. They share the same tags as the producedNode.
final SemanticsNode producedNode = cachedSemanticsNode!;
for (final SemanticsNode node in semanticsNodes) {
if (node != producedNode) {
if (parentData?.tagsForChildren != null) {
node.tags ??= <SemanticsTag>{};
node.tags!.addAll(parentData!.tagsForChildren!);
} else if (node.tags?.isEmpty ?? false) {
node.tags = null;
}
}
}
}
/// Builds the semantics subtree under the [cachedSemanticsNode].
void _buildSemanticsSubtree({required Set<int> usedSemanticsIds}) {
final children = <SemanticsNode>[];
for (final _RenderObjectSemantics child in _children) {
assert(child.shouldFormSemanticsNode);
// Cached semantics node may be part of sibling merging group prior
// to this update. In this case, the semantics node may continue to
// be reused in that sibling merging group.
if (child.cachedSemanticsNode != null &&
usedSemanticsIds.contains(child.cachedSemanticsNode!.id)) {
child.markNeedsBuild();
child.cachedSemanticsNode = null;
}
child._buildSemantics(usedSemanticsIds: usedSemanticsIds);
children.addAll(child.semanticsNodes);
}
final SemanticsNode node = cachedSemanticsNode!;
children.removeWhere(shouldDrop);
if (configProvider.effective.isSemanticBoundary) {
renderObject.assembleSemanticsNode(node, configProvider.effective, children);
} else {
node.updateWith(config: configProvider.effective, childrenInInversePaintOrder: children);
}
}
void _produceSemanticsNode({required Set<int> usedSemanticsIds}) {
assert(!built);
built = true;
final SemanticsNode node = cachedSemanticsNode ??= _createSemanticsNode();
semanticsNodes.add(node);
node
..isMergedIntoParent = (parentData?.mergeIntoParent ?? false)
..tags = parentData?.tagsForChildren;
_updateSemanticsNodeGeometry();
_mergeSiblingGroup(usedSemanticsIds);
_buildSemanticsSubtree(usedSemanticsIds: usedSemanticsIds);
}
SemanticsNode _createSemanticsNode() {
if (isRoot) {
return SemanticsNode.root(
showOnScreen: owner.renderObject.showOnScreen,
owner: owner.renderObject.owner!.semanticsOwner!,
);
}
return SemanticsNode(showOnScreen: owner.renderObject.showOnScreen);
}
void _mergeSiblingGroup(Set<int> usedSemanticsIds) {
for (final List<_SemanticsFragment> group in siblingMergeGroups) {
SemanticsConfiguration? configuration;
SemanticsNode? node;
final explicitChildren = <_RenderObjectSemantics>[];
for (final fragment in group) {
if (fragment is _RenderObjectSemantics) {
if (fragment.shouldFormSemanticsNode) {
explicitChildren.add(fragment);
assert(fragment.configToMergeUp == null);
continue;
}
explicitChildren.addAll(fragment._children);
}
if (fragment.configToMergeUp != null) {
fragment.mergesToSibling = true;
node ??= fragment.owner.cachedSemanticsNode;
configuration ??= SemanticsConfiguration();
configuration.absorb(fragment.configToMergeUp!);
}
}
final childrenNodes = <SemanticsNode>[];
for (final explicitChild in explicitChildren) {
explicitChild._buildSemantics(usedSemanticsIds: usedSemanticsIds);
childrenNodes.addAll(explicitChild.semanticsNodes);
}
// Can be null if all fragments in switchableFragments are marked as explicit.
if (configuration != null) {
if (node == null || usedSemanticsIds.contains(node.id)) {
node = SemanticsNode(showOnScreen: renderObject.showOnScreen);
}
usedSemanticsIds.add(node.id);
for (final fragment in group) {
if (fragment.configToMergeUp != null) {
fragment.owner.built = true;
fragment.owner.cachedSemanticsNode = node;
}
}
node.updateWith(config: configuration, childrenInInversePaintOrder: childrenNodes);
_producedSiblingNodesAndOwners[node] = group;
semanticsNodes.add(node);
final Set<SemanticsTag> tags = group
.map<Set<SemanticsTag>?>(
(_SemanticsFragment fragment) => fragment.owner.parentData!.tagsForChildren,
)
.whereType<Set<SemanticsTag>>()
.expand<SemanticsTag>((Set<SemanticsTag> tags) => tags)
.toSet();
// This fragment is only allowed to add tags into the node instead of
// cleaning it since some of the tags may be added by the parent fragment
// who actually take these node as their siblings.
//
// It will be that fragment's responsibility to clean up the tags.
//
// This is the same for the transform as well.
//
// See _SwitchableFragment.compileSemanticsNodes
if (tags.isNotEmpty) {
if (node.tags == null) {
node.tags = tags;
} else {
node.tags!.addAll(tags);
}
}
node.isMergedIntoParent = parentData?.mergeIntoParent ?? false;
}
}
_updateSiblingNodesGeometries();
}
/// Updates the semantics geometry of the cached semantics node.
void _updateSemanticsNodeGeometry() {
final SemanticsNode node = cachedSemanticsNode!;
final _SemanticsGeometry nodeGeometry = geometry!;
final bool isSemanticsHidden =
configProvider.original.isHidden ||
(!(parentData?.mergeIntoParent ?? false) && nodeGeometry.hidden);
node
..rect = nodeGeometry.rect
..transform = nodeGeometry.transform
..parentSemanticsClipRect = nodeGeometry.semanticsClipRect
..parentPaintClipRect = nodeGeometry.paintClipRect;
if (configProvider.effective.isHidden != isSemanticsHidden) {
configProvider.updateConfig((SemanticsConfiguration config) {
config.isHidden = isSemanticsHidden;
});
}
}
void _updateSiblingNodesGeometries() {
final _SemanticsGeometry mainGeometry = geometry!;
for (final MapEntry<SemanticsNode, List<_SemanticsFragment>> entry
in _producedSiblingNodesAndOwners.entries) {
Rect? rect;
Rect? semanticsClipRect;
Rect? paintClipRect;
for (final _SemanticsFragment fragment in entry.value) {
if (fragment.owner.shouldFormSemanticsNode) {
continue;
}
final _SemanticsGeometry parentGeometry = _SemanticsGeometry.computeChildGeometry(
parentTransform: mainGeometry.transform,
parentSemanticsClipRect: mainGeometry.semanticsClipRect,
parentPaintClipRect: mainGeometry.paintClipRect,
parent: this,
child: fragment.owner,
);
final Rect rectInFragmentOwnerCoordinates =
parentGeometry.semanticsClipRect?.intersect(
fragment.owner.renderObject.semanticBounds,
) ??
fragment.owner.renderObject.semanticBounds;
final Rect rectInParentCoordinates = MatrixUtils.transformRect(
parentGeometry.transform,
rectInFragmentOwnerCoordinates,
);
rect = rect?.expandToInclude(rectInParentCoordinates) ?? rectInParentCoordinates;
if (parentGeometry.semanticsClipRect != null) {
final Rect rect = MatrixUtils.transformRect(
parentGeometry.transform,
parentGeometry.semanticsClipRect!,
);
semanticsClipRect = semanticsClipRect?.intersect(rect) ?? rect;
}
if (parentGeometry.paintClipRect != null) {
final Rect rect = MatrixUtils.transformRect(
parentGeometry.transform,
parentGeometry.paintClipRect!,
);
paintClipRect = paintClipRect?.intersect(rect) ?? rect;
}
}
final SemanticsNode node = entry.key;
node
..rect = rect!
..transform =
null // transform has be taking into account when
// calculating the rect.
..parentSemanticsClipRect = semanticsClipRect
..parentPaintClipRect = paintClipRect;
}
}
/// The [renderObject]'s semantics information has changed.
void markNeedsUpdate() {
final SemanticsNode? producedSemanticsNode = cachedSemanticsNode;
// Dirty the semantics tree starting at `this` until we have reached a
// RenderObject that is a semantics boundary. All semantics past this
// RenderObject are still up-to date. Therefore, we will later only rebuild
// the semantics subtree starting at the identified semantics boundary.
final bool wasSemanticsBoundary =
producedSemanticsNode != null && configProvider.wasSemanticsBoundary;
configProvider.clear();
_containsIncompleteFragment = false;
var mayProduceSiblingNodes = configProvider.effective.childConfigurationsDelegate != null;
bool isEffectiveSemanticsBoundary =
configProvider.effective.isSemanticBoundary && wasSemanticsBoundary;
RenderObject node = renderObject;
// The sibling nodes will be attached to the parent of immediate semantics
// 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 != null && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
if (node != renderObject && node._semantics.parentDataDirty && !mayProduceSiblingNodes) {
break;
}
node._semantics.geometry = null;
node._semantics.parentData = null;
node._semantics._blocksPreviousSibling = null;
// Since this node is a semantics boundary, the produced sibling nodes will
// be attached to the parent semantics boundary. Thus, these sibling nodes
// will not be carried to the next loop.
if (isEffectiveSemanticsBoundary) {
mayProduceSiblingNodes = false;
}
mayProduceSiblingNodes |=
node._semantics.configProvider.effective.childConfigurationsDelegate != null;
node = node.parent!;
// If node._semantics.built is false, this branch is currently blocked.
// In that case, it should continue dirty upward until it reach a
// unblocked semantics boundary because blocked branch will not rebuild
// semantics during PipelineOwner.flushSemantics.
//
// If we stop here and not dirty the rendering parent and a flush semantics
// is called, it will end up with a blocked branch where some sub-branch
// is dirty. There won't be a way to rebuild these dirty sub-branch
// without rebuilding the entire blocked branch (which is costly) when the
// branch is later unblocked.
isEffectiveSemanticsBoundary =
node._semantics.configProvider.effective.isSemanticBoundary && node._semantics.built;
}
if (node != renderObject && producedSemanticsNode != null && node._semantics.parentDataDirty) {
// If `this` node has already been added to [owner._nodesNeedingSemantics]
// remove it as it is no longer guaranteed that its semantics
// node will continue to be in the tree. If it still is in the tree, the
// ancestor `node` added to [owner._nodesNeedingSemantics] at the end of
// this block will ensure that the semantics of `this` node actually gets
// updated.
// (See semantics_10_test.dart for an example why this is required).
renderObject.owner!._nodesNeedingSemantics.remove(renderObject);
}
if (!node._semantics.parentDataDirty) {
if (renderObject.owner != null) {
assert(node._semantics.configProvider.effective.isSemanticBoundary || node.parent == null);
if (renderObject.owner!._nodesNeedingSemantics.add(node)) {
renderObject.owner!.requestVisualUpdate();
}
}
}
}
void _marksConflictsInMergeGroup(List<_SemanticsFragment> mergeGroup, {bool isMergeUp = false}) {
final hasSiblingConflict = <_SemanticsFragment>{};
for (var i = 0; i < mergeGroup.length; i += 1) {
final _SemanticsFragment fragment = mergeGroup[i];
// Remove old value
fragment.markSiblingConfigurationConflict(false);
if (fragment.configToMergeUp == null) {
continue;
}
if (isMergeUp && !configProvider.original.isCompatibleWith(fragment.configToMergeUp)) {
hasSiblingConflict.add(fragment);
}
final siblingLength = i;
for (var j = 0; j < siblingLength; j += 1) {
final _SemanticsFragment siblingFragment = mergeGroup[j];
if (!fragment.configToMergeUp!.isCompatibleWith(siblingFragment.configToMergeUp)) {
hasSiblingConflict.add(fragment);
hasSiblingConflict.add(siblingFragment);
}
}
}
for (final fragment in hasSiblingConflict) {
fragment.markSiblingConfigurationConflict(true);
}
}
/// Removes any cache stored in this object as if it is newly created.
void clear() {
built = false;
cachedSemanticsNode = null;
parentData = null;
geometry = null;
_blocksPreviousSibling = null;
_containsIncompleteFragment = false;
mergeUp.clear();
siblingMergeGroups.clear();
_children.clear();
semanticsNodes.clear();
configProvider.clear();
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
return _getNonBlockedChildren()
.map<DiagnosticsNode>((_RenderObjectSemantics child) => child.toDiagnosticsNode())
.toList();
}
@protected
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('owner', describeIdentity(renderObject)));
properties.add(
FlagProperty('noParentData', value: parentData == null, ifTrue: 'NO PARENT DATA'),
);
properties.add(
FlagProperty(
'semanticsBlock',
value: configProvider.effective.isBlockingSemanticsOfPreviouslyPaintedNodes,
ifTrue: 'BLOCK PREVIOUS',
),
);
if (contributesToSemanticsTree) {
final String semanticsNodeStatus;
if (built) {
semanticsNodeStatus = 'formed ${cachedSemanticsNode?.id}';
} else if (shouldFormSemanticsNode) {
semanticsNodeStatus = 'needs build';
} else {
semanticsNodeStatus = 'no semantics node';
}
properties.add(StringProperty('formedSemanticsNode', semanticsNodeStatus, quoted: false));
}
properties.add(
FlagProperty(
'isSemanticBoundary',
value: configProvider.effective.isSemanticBoundary,
ifTrue: 'semantic boundary',
),
);
properties.add(
FlagProperty('blocksSemantics', value: isBlockingPreviousSibling, ifTrue: 'BLOCKS SEMANTICS'),
);
if (contributesToSemanticsTree && siblingMergeGroups.isNotEmpty) {
properties.add(StringProperty('Sibling group', siblingMergeGroups.toString(), quoted: false));
}
}
}
/// Dumps the render object semantics tree.
void debugDumpRenderObjectSemanticsTree() {
debugPrint(_debugCollectRenderObjectSemanticsTrees());
}
String _debugCollectRenderObjectSemanticsTrees() {
if (RendererBinding.instance.renderViews.isEmpty) {
return 'No render tree root was added to the binding.';
}
return <String>[
for (final RenderObject renderView in RendererBinding.instance.renderViews)
renderView._semantics.toStringDeep(),
].join('\n\n');
}
/// Helper class that keeps track of the geometry of a [SemanticsNode].
///
/// It is used to annotate a [SemanticsNode] with the current information for
/// [SemanticsNode.rect] and [SemanticsNode.transform].
@immutable
final class _SemanticsGeometry {
/// The `paintClipRect` may be null if no clip is to be applied.
const _SemanticsGeometry({
required this.paintClipRect,
required this.semanticsClipRect,
required this.transform,
required this.rect,
required this.hidden,
});
factory _SemanticsGeometry.root(Rect rect) {
return _SemanticsGeometry(
paintClipRect: null,
semanticsClipRect: null,
transform: Matrix4.identity(),
hidden: false,
rect: rect,
);
}
/// Value for [SemanticsNode.transform].
final Matrix4 transform;
/// Value for [SemanticsNode.parentSemanticsClipRect].
final Rect? semanticsClipRect;
/// Value for [SemanticsNode.parentPaintClipRect].
final Rect? paintClipRect;
/// Value for [SemanticsNode.rect].
final Rect rect;
/// Whether the semantics node is completely clipped from ui, i.e. by
/// paintClipRect, but is still present in semantics tree.
final bool hidden;
static _SemanticsGeometry computeChildGeometry({
required Matrix4? parentTransform,
required Rect? parentPaintClipRect,
required Rect? parentSemanticsClipRect,
required _RenderObjectSemantics parent,
required _RenderObjectSemantics child,
}) {
RenderObject childRenderObject = child.renderObject;
final RenderObject parentRenderObject = parent.renderObject;
final childToCommonAncestor = <RenderObject>[childRenderObject];
// Find the path from childRenderObject to parentRenderObject.
// Currently the framework assumes that parentRenderObject is an ancestor of
// childRenderObject.
while (childRenderObject.depth > parentRenderObject.depth) {
assert(
childRenderObject.parent != null,
'The render object of $parent is not an ancestor of the render object of $child.',
);
childRenderObject = childRenderObject.parent!;
childToCommonAncestor.add(childRenderObject);
}
assert(childToCommonAncestor.length >= 2);
assert(identical(childRenderObject, parentRenderObject));
// Calculate clips and transform.
Rect? paintClipRect;
Rect? semanticsClipRect;
final transform = Matrix4.identity();
// Traverse from `parent`'s render object to `child`'s.
for (int i = childToCommonAncestor.length - 1; i > 0; i -= 1) {
final RenderObject nodeParent = childToCommonAncestor[i];
final RenderObject node = childToCommonAncestor[i - 1];
final Rect? localPaintClipInParent = _transformRect(
nodeParent.describeApproximatePaintClip(node),
transform, // paint transform of nodeParent in relation to parent
MatrixUtils.transformRect,
);
final Rect? localSemanticsClipInParent = _transformRect(
nodeParent.describeSemanticsClip(node),
transform, // paint transform of nodeParent in relation to parent
MatrixUtils.transformRect,
);
paintClipRect = _intersectRects(paintClipRect, localPaintClipInParent);
semanticsClipRect =
localSemanticsClipInParent ??
semanticsClipRect?.intersect(localPaintClipInParent ?? semanticsClipRect);
nodeParent.applyPaintTransform(node, transform);
}
// Apply the parent paint / semantics clipping.
semanticsClipRect =
semanticsClipRect ?? _intersectRects(paintClipRect, parentSemanticsClipRect);
paintClipRect = _intersectRects(paintClipRect, parentPaintClipRect);
if (paintClipRect != null || semanticsClipRect != null) {
final Matrix4 inverted = transform.clone();
final hasInverse = inverted.invert() != 0.0;
semanticsClipRect = hasInverse
? _transformRect(semanticsClipRect, inverted, MatrixUtils.transformRect)
: null;
paintClipRect = hasInverse
? _transformRect(paintClipRect, inverted, MatrixUtils.transformRect)
: null;
}
if (parentTransform != null) {
MatrixUtils.multiplyInPlace(parentTransform, transform);
}
Rect rect =
semanticsClipRect?.intersect(child.renderObject.semanticBounds) ??
child.renderObject.semanticBounds;
var isRectHidden = false;
if (paintClipRect != null) {
final Rect paintRect = paintClipRect.intersect(rect);
isRectHidden = paintRect.isEmpty && !rect.isEmpty;
if (!isRectHidden) {
rect = paintRect;
}
}
return _SemanticsGeometry(
transform: transform,
paintClipRect: paintClipRect,
semanticsClipRect: semanticsClipRect,
rect: rect,
hidden: isRectHidden,
);
}
/// From parent to child coordinate system.
static Rect? _transformRect(
Rect? rect,
Matrix4 transform, [
_TransformRect apply = MatrixUtils.inverseTransformRect,
]) {
if (rect == null) {
return null;
}
if (rect.isEmpty || transform.isZero()) {
return Rect.zero;
}
return apply(transform, rect);
}
static Rect? _intersectRects(Rect? a, Rect? b) {
if (b == null) {
return a;
}
return a?.intersect(b) ?? b;
}
}
/// A class that creates [DiagnosticsNode] by wrapping [RenderObject.debugCreator].
///
/// Attach a [DiagnosticsDebugCreator] into [FlutterErrorDetails.informationCollector]
/// when a [RenderObject.debugCreator] is available. This will lead to improved
/// error message.
class DiagnosticsDebugCreator extends DiagnosticsProperty<Object> {
/// Create a [DiagnosticsProperty] with its [value] initialized to input
/// [RenderObject.debugCreator].
DiagnosticsDebugCreator(Object value)
: super('debugCreator', value, level: DiagnosticLevel.hidden);
}