| // 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 'sliver.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; | |
| // Deferred updates would otherwise retain their render objects until | |
| // this owner is disposed; re-enabling semantics rebuilds from scratch. | |
| _deferredNodesNeedingSemanticsGeometryUpdate.clear(); | |
| onSemanticsOwnerDisposed?.call(); | |
| } | |
| } | |
| void _didDisposeSemanticsHandle() { | |
| assert(_semanticsOwner != null); | |
| _outstandingSemanticsHandles -= 1; | |
| _updateSemanticsOwner(); | |
| } | |
| bool _debugDoingSemantics = false; | |
| /// Nodes that need semantics update. | |
| /// | |
| /// When a [_RenderObjectSemantics.markNeedsUpdate] is called, the [_RenderObjectSemantics]'s | |
| /// closest semantics boundary ancestor is added to this set. | |
| /// | |
| /// All nodes in this set are guaranteed to be semantics boundaries. | |
| final Set<RenderObject> _nodesNeedingSemanticsUpdate = <RenderObject>{}; | |
| /// Nodes that need semantics geometry update. | |
| /// | |
| /// Nodes are added when their [_RenderObjectSemantics.markNeedsUpdate]s are called. | |
| /// | |
| /// This is used to track nodes that need their semantics geometry updated. | |
| /// Compare to [_nodesNeedingSemanticsUpdate], which tracks semantics boundaries of dirty nodes, | |
| /// this set only tracks the dirty nodes that need their semantics geometry updated directly. | |
| final Set<RenderObject> _nodesNeedingSemanticsGeometryUpdate = <RenderObject>{}; | |
| /// Nodes whose geometry update was skipped by [flushSemantics] because they | |
| /// were not part of the semantics tree at the time, e.g. their branch was | |
| /// blocked by [BlockSemantics]. | |
| /// | |
| /// A blocked node can still move, so the update is retried on every flush | |
| /// instead of dropped; otherwise the node would rejoin the tree with stale | |
| /// geometry. | |
| final Set<RenderObject> _deferredNodesNeedingSemanticsGeometryUpdate = <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 a top-down order to be more performant. Otherwise, a parent | |
| // change can invalidate the whole subtree. | |
| final List<RenderObject> nodesToProcess = | |
| _nodesNeedingSemanticsUpdate | |
| .where((RenderObject object) => !object._needsLayout && object.owner == this) | |
| .toList() | |
| ..sort((RenderObject a, RenderObject b) => a.depth - b.depth); | |
| _nodesNeedingSemanticsUpdate.clear(); | |
| if (!kReleaseMode) { | |
| FlutterTimeline.startSync('Semantics.updateChildren'); | |
| } | |
| final RenderObject? rootNode = this.rootNode; | |
| 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. | |
| // | |
| // We MUST NOT update the parent data now since it may create a gap of | |
| // render objects with dirty parent data when this branch later rejoins | |
| // the rendering tree. | |
| continue; | |
| } | |
| node._semantics.updateChildren(); | |
| } | |
| if (!kReleaseMode) { | |
| FlutterTimeline.finishSync(); | |
| } | |
| assert(() { | |
| assert(nodesToProcess.isEmpty || rootNode != null); | |
| if (rootNode != null) { | |
| _RenderObjectSemantics.debugCheckForParentData(rootNode); | |
| } | |
| return true; | |
| }()); | |
| if (!kReleaseMode) { | |
| FlutterTimeline.startSync('Semantics.ensureGeometry'); | |
| } | |
| // It is possible the updateChildren above caused some nodes to be added | |
| // to _nodesNeedingSemanticsGeometryUpdate. Therefore, we need to | |
| // process them here. | |
| // | |
| // Deferred geometry updates are retried as well; if their branch | |
| // rejoined the tree in the updateChildren phase above, they can now be | |
| // applied. | |
| if (_deferredNodesNeedingSemanticsGeometryUpdate.isNotEmpty) { | |
| _nodesNeedingSemanticsGeometryUpdate.addAll(_deferredNodesNeedingSemanticsGeometryUpdate); | |
| _deferredNodesNeedingSemanticsGeometryUpdate.clear(); | |
| } | |
| final nodesToProcessGeometry = <RenderObject>[]; | |
| for (final RenderObject object in _nodesNeedingSemanticsGeometryUpdate) { | |
| if (object._needsLayout || object.owner != this) { | |
| // A node that still needs layout marks itself again after its next | |
| // layout; a node that left this owner no longer needs the update. | |
| continue; | |
| } | |
| if (object._semantics.parentDataDirty) { | |
| // Not part of the semantics tree right now (e.g. blocked by | |
| // BlockSemantics), but the node may still move. Defer the update | |
| // until the branch rejoins instead of dropping it. | |
| _deferredNodesNeedingSemanticsGeometryUpdate.add(object); | |
| continue; | |
| } | |
| nodesToProcessGeometry.add(object); | |
| } | |
| _nodesNeedingSemanticsGeometryUpdate.clear(); | |
| // For every node in this list, needs to clear geometry immediate _RenderObjectSemantics | |
| // that forms semantics node in the subtree. | |
| // | |
| // Case 1, this node forms semantics node, then clear the geometry of this node if it is | |
| // a relayout boundary. Otherwise, the parent should handle clearing the geometry. | |
| // | |
| // Case 2, this node does not form semantics node, then clear immediate children's | |
| // geometry if they form semantics node because this node's transform and clip may affect | |
| // the geometry of the children. | |
| for (final node in nodesToProcessGeometry) { | |
| if (node._semantics.shouldFormSemanticsNode && node._semantics.geometryDirty) { | |
| // This node is already dirty, skip it. | |
| continue; | |
| } | |
| if (node._semantics.shouldFormSemanticsNode && (node._isRelayoutBoundary ?? false)) { | |
| // If this node is a relayout boundary, it can change size without parent relayout. | |
| // In this case, we still need to clear its geometry. | |
| node._semantics.geometry = null; | |
| continue; | |
| } | |
| if (!node._semantics.contributesToSemanticsTree) { | |
| // This node merely presents its subtree in the mergeup, so we need to clear | |
| // the geometry for all the semantics nodes in the mergeup. | |
| for (final _RenderObjectSemantics child | |
| in node._semantics.mergeUp.whereType<_RenderObjectSemantics>()) { | |
| if (child.shouldFormSemanticsNode) { | |
| child.geometry = null; | |
| } else { | |
| // Even though this node does not form a semantics node, it still | |
| // represents a group of render objects in the subtree, where a node that | |
| // forms a semantics node is in its _children. | |
| for (final _RenderObjectSemantics nodeInSubtree in child._children) { | |
| assert(nodeInSubtree.shouldFormSemanticsNode); | |
| nodeInSubtree.geometry = null; | |
| } | |
| } | |
| } | |
| continue; | |
| } | |
| // If we reach here, this node either forms a node but not a relayout boundary, | |
| // or it does not form a node but still contributes to the semantics tree. | |
| // In both cases, we need to clear the geometry for all the semantics nodes in | |
| // the subtree. | |
| for (final _RenderObjectSemantics child in node._semantics._children) { | |
| child.geometry = null; | |
| } | |
| } | |
| // Used to invalidate the [_RenderObjectSemantics.firstAncestorNodeWithCleanGeometry] cache. | |
| // | |
| // Multiple items in [nodesToProcessGeometry] often share the same first | |
| // ancestor, making redundant searches expensive. Passing this token allows | |
| // nodes to reuse cached ancestor lookups. | |
| // | |
| // See [_RenderObjectSemantics.computeAncestorInfo] for details. | |
| final treeShapeToken = Object(); | |
| final nodeToEnsureGeometry = <_RenderObjectSemantics>{}; | |
| for (final node in nodesToProcessGeometry) { | |
| // We need a node that has clean geometry to start from. | |
| node._semantics.computeAncestorInfo(treeShapeToken); | |
| if (node._semantics.firstAncestorNodeWithCleanGeometry != null) { | |
| nodeToEnsureGeometry.add(node._semantics.firstAncestorNodeWithCleanGeometry!); | |
| } | |
| } | |
| for (final _RenderObjectSemantics node | |
| in nodeToEnsureGeometry.toList()..sort( | |
| (_RenderObjectSemantics a, _RenderObjectSemantics b) => | |
| a.renderObject.depth - b.renderObject.depth, | |
| )) { | |
| node.ensureGeometry(); | |
| } | |
| if (!kReleaseMode) { | |
| FlutterTimeline.finishSync(); | |
| } | |
| if (!kReleaseMode) { | |
| FlutterTimeline.startSync('Semantics.ensureSemanticsNode'); | |
| } | |
| for (final RenderObject node in nodesToProcess.reversed) { | |
| node._semantics.computeAncestorInfo(treeShapeToken); | |
| final targets = <_RenderObjectSemantics>[]; | |
| if (node._semantics.geometryDirty) { | |
| if (node._semantics.firstAncestorNodeWithCleanGeometry != null) { | |
| targets.add(node._semantics.firstAncestorNodeWithCleanGeometry!); | |
| } | |
| } else { | |
| // When this node is a semantics boundary and a layout boundary and its | |
| // geometry becomes invisible after the ensureGeometry call above, | |
| // the parent of this node will have to update its semantics subtree to remove | |
| // this node from its children. | |
| if (!node._semantics.geometry!.isVisible && !node._semantics.isRoot) { | |
| final _RenderObjectSemantics? parentInSemanticsTree = | |
| node._semantics.parentInSemanticsTree; | |
| if (parentInSemanticsTree != null) { | |
| if (!parentInSemanticsTree.geometryDirty) { | |
| targets.add(parentInSemanticsTree); | |
| } else { | |
| final _RenderObjectSemantics? firstAncestorNodeWithCleanGeometry = | |
| parentInSemanticsTree.firstAncestorNodeWithCleanGeometry; | |
| // firstAncestorNodeWithCleanGeometry can be null if this is a blocked branch. | |
| if (firstAncestorNodeWithCleanGeometry != null) { | |
| targets.add(firstAncestorNodeWithCleanGeometry); | |
| } | |
| } | |
| } | |
| } | |
| targets.add(node._semantics); | |
| } | |
| for (final target in targets) { | |
| if (target.parentDataDirty) { | |
| // This node can't be updated since it is not in the tree. | |
| continue; | |
| } | |
| target.ensureSemanticsNode(); | |
| } | |
| } | |
| assert(() { | |
| if (rootNode != null) { | |
| _RenderObjectSemantics.debugCheckForBuilds(rootNode._semantics); | |
| } | |
| return true; | |
| }()); | |
| if (!kReleaseMode) { | |
| FlutterTimeline.finishSync(); | |
| } | |
| _semanticsOwner!.sendSemanticsUpdate(); | |
| for (final PipelineOwner child in _children) { | |
| child.flushSemantics(); | |
| } | |
| assert( | |
| _nodesNeedingSemanticsUpdate.isEmpty, | |
| 'Child PipelineOwners must not dirty nodes in their parent.', | |
| ); | |
| assert( | |
| _nodesNeedingSemanticsGeometryUpdate.isEmpty, | |
| "Child PipelineOwners must not dirty nodes' geometry 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(); | |
| _nodesNeedingSemanticsUpdate.clear(); | |
| _deferredNodesNeedingSemanticsGeometryUpdate.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; | |
| // Returns the closest ancestor that determines whether this RenderObject is | |
| // currently allowed to mutate, and a boolean indicating whether this | |
| // RenderObject is allowed to mutate. | |
| // | |
| // This method must only be called during layout, as mutations are always | |
| // allowed before layout. A null return value indicates another render subtree | |
| // is actively performing layout and, in the process, illegally mutating this | |
| // RenderObject's subtree. | |
| (RenderObject, bool)? get _debugClosestMutationRoot { | |
| return switch (this) { | |
| // This subtree is being mutated in a layout callback. | |
| RenderObject(_doingThisLayoutWithCallback: true) => (this, true), | |
| // A different part of the render tree is doing a layout callback, | |
| // and this subtree is being reparented there, due to global key | |
| // reparenting. | |
| RenderObject( | |
| owner: PipelineOwner(_debugAllowMutationsToDirtySubtrees: true), | |
| _needsLayout: true, | |
| ) => | |
| (this, true), | |
| // This is the node currently doing layout. | |
| RenderObject(_debugMutationsLocked: true) => (this, false), | |
| RenderObject() => debugLayoutParent?._debugClosestMutationRoot, | |
| }; | |
| } | |
| /// 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 isMutationAllowed; | |
| 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) { | |
| isMutationAllowed = true; | |
| return true; | |
| } | |
| final RenderObject? activeLayoutRoot; | |
| // If there is no mutation root, this subtree is likely being mutated by a different subtree | |
| // which is not allowed. | |
| (activeLayoutRoot, isMutationAllowed) = _debugClosestMutationRoot ?? (null, false); | |
| if (isMutationAllowed) { | |
| return true; | |
| } | |
| final RenderObject debugActiveLayout = RenderObject.debugActiveLayout!; | |
| final culpritMethodName = debugActiveLayout.debugDoingThisLayout | |
| ? 'performLayout' | |
| : 'performResize'; | |
| final culpritFullMethodName = '${debugActiveLayout.runtimeType}.$culpritMethodName'; | |
| 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 isMutationAllowed; | |
| } | |
| /// 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], and becomes false during the call to [detach]. | |
| @nonVirtual | |
| 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. Always returns false in non-debug builds. | |
| /// | |
| /// It is intended to be used by tests and asserts. | |
| bool get debugNeedsLayout { | |
| if (!kDebugMode) { | |
| return false; | |
| } | |
| return _needsLayout; | |
| } | |
| 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. | |
| /// | |
| /// Layout callbacks are a special part of this render object's layout phase: | |
| /// they are allowed to mutate child lists and dirty render subtrees while the | |
| /// callback is executing. This flag lets debug-only assertions distinguish | |
| /// that special callback phase from the regular [performLayout] body. | |
| /// | |
| /// This does not imply that a widget rebuild is currently in progress. A | |
| /// layout callback can trigger rebuild work by mutating the render or element | |
| /// tree, but this flag only describes the synchronous callback passed to | |
| /// [invokeLayoutCallback]. | |
| 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. Always returns false in non-debug builds. | |
| /// | |
| /// 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 { | |
| if (!kDebugMode) { | |
| return false; | |
| } | |
| return _needsPaint; | |
| } | |
| 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. Always returns false in non-debug builds. | |
| /// | |
| /// It is intended to be used by tests and asserts. | |
| bool get debugNeedsCompositedLayerUpdate { | |
| if (!kDebugMode) { | |
| return false; | |
| } | |
| return _needsCompositedLayerUpdate; | |
| } | |
| 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!._nodesNeedingSemanticsUpdate.add(this); | |
| owner!._nodesNeedingSemanticsGeometryUpdate.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, | |
| ) { | |
| // 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() { | |
| if (_hasPendingSystemFontsDidChangeCallBack) { | |
| return; | |
| } | |
| _hasPendingSystemFontsDidChangeCallBack = true; | |
| // The system fonts notification can arrive during any scheduler phase | |
| // (e.g. midFrameMicrotasks on web when fonts load asynchronously). | |
| // scheduleFrameCallback is safe to call during any phase; if called | |
| // mid-frame, the callback runs in the next frame's transient callback | |
| // phase since handleBeginFrame snapshots and replaces _transientCallbacks. | |
| 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 || (_properties.identifier != null); | |
| config.explicitChildNodes = explicitChildNodes; | |
| config.isBlockingUserActions = blockUserActions; | |
| if (localeForSubtree != null) { | |
| 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.accessibilityFocusBlockType != null) { | |
| config.accessibilityFocusBlockType = _properties.accessibilityFocusBlockType!; | |
| } | |
| 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.hitTestBehavior != null) { | |
| config.hitTestBehavior = _properties.hitTestBehavior!; | |
| } | |
| if (_properties.inputType != null) { | |
| config.inputType = _properties.inputType!; | |
| } | |
| if (_properties.minValue != null) { | |
| config.minValue = _properties.minValue; | |
| } | |
| if (_properties.maxValue != null) { | |
| config.maxValue = _properties.maxValue; | |
| } | |
| // 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.accessibilityFocusBlockType, | |
| }); | |
| /// 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 AccessibilityFocusBlockType? accessibilityFocusBlockType; | |
| /// 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 && | |
| other.accessibilityFocusBlockType == accessibilityFocusBlockType && | |
| setEquals<SemanticsTag>(other.tagsForChildren, tagsForChildren); | |
| } | |
| @override | |
| int get hashCode { | |
| return Object.hash( | |
| mergeIntoParent, | |
| blocksUserActions, | |
| explicitChildNodes, | |
| localeForChildren, | |
| accessibilityFocusBlockType, | |
| 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 | |
| /// 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; | |
| _RenderObjectSemantics? parentInSemanticsTree; | |
| // Parent in render object order. | |
| _RenderObjectSemantics? get parent => renderObject.parent?._semantics; | |
| bool get parentDataDirty { | |
| if (isRoot) { | |
| return false; | |
| } | |
| return parentData == null; | |
| } | |
| bool get geometryDirty { | |
| if (isRoot) { | |
| return false; | |
| } | |
| return geometry == null; | |
| } | |
| /// Validates the [firstAncestorNodeWithCleanGeometry] cache. | |
| /// | |
| /// Updated by [computeAncestorInfo] during [PipelineOwner.flushSemantics] to | |
| /// prevent redundant ancestor lookups within a single flush cycle. | |
| Object _currentTreeShapeToken = const Object(); | |
| /// The nearest semantics-forming ancestor node with clean geometry. | |
| /// | |
| /// Cached by [computeAncestorInfo] during [PipelineOwner.flushSemantics]. | |
| _RenderObjectSemantics? firstAncestorNodeWithCleanGeometry; | |
| /// Finds and caches the nearest semantics-forming ancestor with up-to-date geometry. | |
| /// | |
| /// The [treeShapeToken] parameter prevents redundant calculation by verifying | |
| /// whether the tree shape changed since the previous lookup. | |
| /// | |
| /// This method requires all dirty nodes in [PipelineOwner._nodesNeedingSemanticsUpdate] | |
| /// to have completed their [updateChildren] calls. | |
| /// | |
| /// See phase 2 in the [_RenderObjectSemantics] documentation. | |
| void computeAncestorInfo(Object treeShapeToken) { | |
| if (treeShapeToken == _currentTreeShapeToken) { | |
| return; | |
| } | |
| _currentTreeShapeToken = treeShapeToken; | |
| if (isRoot) { | |
| firstAncestorNodeWithCleanGeometry = this; | |
| return; | |
| } | |
| firstAncestorNodeWithCleanGeometry = null; | |
| if (parentDataDirty) { | |
| return; | |
| } | |
| _RenderObjectSemantics? next; | |
| if (shouldFormSemanticsNode) { | |
| if (!geometryDirty) { | |
| firstAncestorNodeWithCleanGeometry = this; | |
| } | |
| next = parentInSemanticsTree; | |
| } else { | |
| next = this; | |
| while (!next!.parentDataDirty && !next.shouldFormSemanticsNode) { | |
| next = next.parent; | |
| // The next should be at least at or below the root node. Since | |
| // root node's parentData is always not dirty and form semantics | |
| // node, the loop will terminate at or before the root node. | |
| assert(next != null); | |
| } | |
| } | |
| if (next == null) { | |
| return; | |
| } | |
| if (firstAncestorNodeWithCleanGeometry == null) { | |
| next.computeAncestorInfo(treeShapeToken); | |
| firstAncestorNodeWithCleanGeometry = next.firstAncestorNodeWithCleanGeometry; | |
| } | |
| } | |
| /// 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 => parent == null; | |
| bool get _needsMergingSiblingNodesIntoSelf { | |
| return configProvider.effective.isMergingSemanticsOfDescendants && | |
| _producedSiblingNodesAndOwners.isNotEmpty; | |
| } | |
| 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); | |
| } | |
| static void debugCheckForBuilds(_RenderObjectSemantics node) { | |
| assert(node.built); | |
| node._children.forEach(debugCheckForBuilds); | |
| } | |
| /// 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 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; | |
| AccessibilityFocusBlockType accessibilityFocusBlockType; | |
| if (parentData?.accessibilityFocusBlockType == AccessibilityFocusBlockType.blockSubtree) { | |
| accessibilityFocusBlockType = AccessibilityFocusBlockType.blockSubtree; | |
| } else { | |
| accessibilityFocusBlockType = configProvider.effective.accessibilityFocusBlockType; | |
| } | |
| // 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, | |
| accessibilityFocusBlockType: accessibilityFocusBlockType, | |
| 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. | |
| final Set<_RenderObjectSemantics> oldChildren = _children.toSet(); | |
| _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) { | |
| for (final _RenderObjectSemantics child in childSemantics._children) { | |
| child.parentInSemanticsTree = childSemantics; | |
| } | |
| // In general geometry is only dirty during the first build or markNeedsLayout. | |
| // In both cases, the renderobject will be added to the list of nodes needing | |
| // geometry update. | |
| // | |
| // This specific case can happen if a parent somwhere above this node decided to | |
| // conditionally include this node. | |
| // | |
| // For example, render object A->B->C, where A is the root and all of them | |
| // creates semantics nodes. | |
| // | |
| // Frame 1: B and C both mark layout dirty, and B suddenly decide not to | |
| // inlcude C in the semantics tree. | |
| // | |
| // In this case, C's geometry will remain dirty because it is considered as | |
| // blocked. | |
| // | |
| // Frame 2: B is marked dirty again and decide C should be included | |
| // in the semantics tree. We will run into this situation where C's geometry | |
| // is dirty but it is not in the _nodesNeedingSemanticsGeometryUpdate. | |
| // | |
| // A node that moved while its branch was blocked has stale, not | |
| // dirty, geometry, so this check misses it. That case is covered by | |
| // PipelineOwner._deferredNodesNeedingSemanticsGeometryUpdate. | |
| if (childSemantics.geometryDirty) { | |
| renderObject.owner!._nodesNeedingSemanticsGeometryUpdate.add(childSemantics.renderObject); | |
| } | |
| _children.add(childSemantics); | |
| } else { | |
| _children.addAll(childSemantics._children); | |
| siblingMergeGroups.addAll(childSemantics.siblingMergeGroups); | |
| } | |
| } | |
| // If this node is the root, then all of its children are in the semantics tree. | |
| // Otherwise, we won't know if this node shouldFormSemanticsNode until the parent | |
| // of this node determines it. | |
| if (isRoot || configProvider.effective.isSemanticBoundary) { | |
| for (final _RenderObjectSemantics child in _children) { | |
| child.parentInSemanticsTree = this; | |
| } | |
| } | |
| oldChildren.removeAll(_children); | |
| for (final removedChild in oldChildren) { | |
| if (removedChild.parentInSemanticsTree == this) { | |
| removedChild.parentInSemanticsTree = null; | |
| } | |
| } | |
| final Set<SemanticsTag>? tags = parentData?.tagsForChildren; | |
| if (tags != null) { | |
| assert(tags.isNotEmpty); | |
| configProvider.updateConfig((SemanticsConfiguration config) { | |
| tags.forEach(config.addTagForChildren); | |
| }); | |
| } | |
| if (accessibilityFocusBlockType != configProvider.effective.accessibilityFocusBlockType) { | |
| configProvider.updateConfig((SemanticsConfiguration config) { | |
| config.accessibilityFocusBlockType = accessibilityFocusBlockType; | |
| }); | |
| } | |
| if (blocksUserAction != configProvider.effective.isBlockingUserActions) { | |
| configProvider.updateConfig((SemanticsConfiguration config) { | |
| config.isBlockingUserActions = blocksUserAction; | |
| }); | |
| } | |
| if (localeForChildren != configProvider.effective.locale) { | |
| configProvider.updateConfig((SemanticsConfiguration config) { | |
| config.locale = localeForChildren; | |
| }); | |
| } | |
| if (accessibilityFocusBlockType != AccessibilityFocusBlockType.none) { | |
| configProvider.updateConfig((SemanticsConfiguration config) { | |
| config.isFocused = null; | |
| }); | |
| } | |
| } | |
| 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, | |
| accessibilityFocusBlockType: childParentData.accessibilityFocusBlockType, | |
| 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; | |
| } | |
| final bool wasParentDataDirty = parentDataDirty; | |
| // Parent data changes may result in node formation changes. | |
| markNeedsBuild(); | |
| parentData = newParentData; | |
| if (wasParentDataDirty) { | |
| geometry = null; | |
| } | |
| 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 the dirty | |
| /// children's subtree in [_children]. | |
| /// | |
| /// This method does 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() { | |
| assert(!geometryDirty); | |
| if (isRoot) { | |
| if (geometry?.rect != renderObject.semanticBounds) { | |
| markNeedsBuild(); | |
| } | |
| geometry = _SemanticsGeometry.root(renderObject.semanticBounds); | |
| } | |
| _updateChildGeometry(onlyDirtyChildren: true); | |
| } | |
| void _updateChildGeometry({bool onlyDirtyChildren = false}) { | |
| assert(geometry != null); | |
| final _SemanticsGeometry parentGeometry = geometry!; | |
| for (final _RenderObjectSemantics child in _children) { | |
| if (onlyDirtyChildren && !child.geometryDirty) { | |
| continue; | |
| } | |
| 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, | |
| )) { | |
| if (onlyDirtyChildren && !explicitSiblingChild.geometryDirty) { | |
| continue; | |
| } | |
| 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}) { | |
| final _SemanticsGeometry? currentGeometry = geometry; | |
| geometry = newGeometry; | |
| if (currentGeometry != null) { | |
| final bool isSemanticsHidden = | |
| configProvider.original.isHidden || | |
| (!(parentData?.mergeIntoParent ?? false) && newGeometry.hidden); | |
| final sizeChanged = currentGeometry.rect.size != newGeometry.rect.size; | |
| final visibilityChanged = configProvider.effective.isHidden != isSemanticsHidden; | |
| if (!sizeChanged && !visibilityChanged) { | |
| return; | |
| } | |
| } | |
| markNeedsBuild(); | |
| _updateChildGeometry(); | |
| } | |
| /// Ensures the semantics nodes from this render object semantics subtree are | |
| /// generated and up to date. | |
| /// | |
| /// This method does 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(shouldFormSemanticsNode); | |
| 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) { | |
| _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) { | |
| if (child.parentDataDirty) { | |
| continue; | |
| } | |
| 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); | |
| final bool isSemanticsHidden = | |
| configProvider.original.isHidden || | |
| (!(parentData?.mergeIntoParent ?? false) && geometry!.hidden); | |
| if (configProvider.effective.isHidden != isSemanticsHidden) { | |
| configProvider.updateConfig((SemanticsConfiguration config) { | |
| config.isHidden = isSemanticsHidden; | |
| }); | |
| } | |
| if (configProvider.effective.isSemanticBoundary) { | |
| if (_needsMergingSiblingNodesIntoSelf) { | |
| // This node needs to merge the sibling nodes in the sub tree. | |
| // | |
| // Create an inner node to hold the configuration and children, and the cachedSemanticsNode will | |
| // hold the inner node and the sibling nodes so that all of them can be merged together. | |
| final innerNode = SemanticsNode(showOnScreen: renderObject.showOnScreen); | |
| renderObject.assembleSemanticsNode(innerNode, configProvider.effective, children); | |
| final config = SemanticsConfiguration() | |
| ..isSemanticBoundary = true | |
| ..isMergingSemanticsOfDescendants = true; | |
| node.updateWith( | |
| config: config, | |
| childrenInInversePaintOrder: <SemanticsNode>[ | |
| innerNode, | |
| ..._producedSiblingNodesAndOwners.keys, | |
| ], | |
| ); | |
| } else { | |
| renderObject.assembleSemanticsNode(node, configProvider.effective, children); | |
| } | |
| } else { | |
| assert(!configProvider.effective.isMergingSemanticsOfDescendants); | |
| node.updateWith(config: configProvider.effective, childrenInInversePaintOrder: children); | |
| } | |
| } | |
| void _produceSemanticsNode({required Set<int> usedSemanticsIds}) { | |
| assert(!built); | |
| semanticsNodes.clear(); | |
| _producedSiblingNodesAndOwners.clear(); | |
| final SemanticsNode node = cachedSemanticsNode ??= _createSemanticsNode(); | |
| node | |
| ..isMergedIntoParent = (parentData?.mergeIntoParent ?? false) | |
| ..tags = parentData?.tagsForChildren; | |
| _updateSemanticsNodeGeometry(); | |
| _mergeSiblingGroup(usedSemanticsIds); | |
| _buildSemanticsSubtree(usedSemanticsIds: usedSemanticsIds); | |
| // At this point, the cachedSemanticsNode should be latest semantics node | |
| // representing this render object and _producedSiblingNodesAndOwners contains the latest | |
| // sibling nodes. | |
| semanticsNodes.add(node); | |
| if (!_needsMergingSiblingNodesIntoSelf) { | |
| semanticsNodes.addAll(_producedSiblingNodesAndOwners.keys); | |
| } | |
| built = true; | |
| } | |
| 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; | |
| 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 = | |
| configProvider.effective.isMergingSemanticsOfDescendants || | |
| (parentData?.mergeIntoParent ?? false); | |
| } | |
| } | |
| _updateSiblingNodesGeometries(); | |
| } | |
| /// Updates the semantics geometry of the cached semantics node. | |
| void _updateSemanticsNodeGeometry() { | |
| final SemanticsNode node = cachedSemanticsNode!; | |
| final _SemanticsGeometry nodeGeometry = geometry!; | |
| node | |
| ..rect = nodeGeometry.rect | |
| ..transform = nodeGeometry.transform | |
| ..parentSemanticsClipRect = nodeGeometry.semanticsClipRect | |
| ..parentPaintClipRect = nodeGeometry.paintClipRect; | |
| } | |
| 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() { | |
| renderObject.owner!._nodesNeedingSemanticsGeometryUpdate.add(renderObject); | |
| 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.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._nodesNeedingSemanticsUpdate] | |
| // 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._nodesNeedingSemanticsUpdate] 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!._nodesNeedingSemanticsUpdate.remove(renderObject); | |
| } | |
| if (!node._semantics.parentDataDirty || node._semantics.isRoot) { | |
| if (renderObject.owner != null) { | |
| assert(node._semantics.configProvider.effective.isSemanticBoundary || node.parent == null); | |
| if (renderObject.owner!._nodesNeedingSemanticsUpdate.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 _children | |
| .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: parentDataDirty, ifTrue: 'NO PARENT DATA')); | |
| properties.add(FlagProperty('geometry', value: geometryDirty, ifTrue: 'NO GEOMETRY')); | |
| properties.add( | |
| FlagProperty( | |
| 'semanticsBlock', | |
| value: configProvider.effective.isBlockingSemanticsOfPreviouslyPaintedNodes, | |
| ifTrue: 'BLOCK PREVIOUS', | |
| ), | |
| ); | |
| if (!parentDataDirty && 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() { | |
| if (RendererBinding.instance.renderViews.isEmpty) { | |
| debugPrint('No render tree root was added to the binding.'); | |
| return; | |
| } | |
| debugPrint( | |
| <String>[ | |
| for (final RenderObject renderView in RendererBinding.instance.renderViews) | |
| _debugCollectRenderObjectSemanticsTrees(renderView), | |
| ].join('\n\n'), | |
| ); | |
| } | |
| String _debugCollectRenderObjectSemanticsTrees(RenderObject root) { | |
| return root._semantics.toStringDeep(); | |
| } | |
| /// 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; | |
| bool get isVisible => !rect.isEmpty && !transform.isZero(); | |
| /// 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); | |
| } |