Greg Spencer 8c271e5cb8
Implement Material MenuBar and MenuAnchor (#112239)
This implements a MenuBar widget that can render a Material menu bar, and a MenuAnchor widget used to create a cascading menu in a region. The menus are drawn in the overlay, while the menu bar itself is in the regular widget tree. Keyboard traversal works between the two.

This implementation of the MenuBar uses MenuAnchor to create a cascading menu that contains widgets representing the menu items. These menu items can be any kind of widget, but are typically SubmenuButtons that host submenus, or MenuItemButtons that have shortcut hints (but don't actually activate the shortcuts) and don't host submenus.

Cascading menus can be created outside of a MenuBar by using a MenuAnchor. They can be either given a specific location to appear (a coordinate), or they can be located by the MenuAnchor region that wraps the control that opens them.

The developer may also create a MenuController to pass to the various menu primitives (MenuBar or MenuAnchor) to associate menus so that they can be traversed together and closed together. Creating a controller is not required.
2022-09-28 14:10:18 -07:00

5116 lines
163 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show Color, Gradient, Image, ImageFilter;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'box.dart';
import 'layer.dart';
import 'layout_helper.dart';
import 'object.dart';
export 'package:flutter/gestures.dart' show
PointerCancelEvent,
PointerDownEvent,
PointerEvent,
PointerMoveEvent,
PointerUpEvent;
/// A base class for render boxes that resemble their children.
///
/// A proxy box has a single child and simply mimics all the properties of that
/// child by calling through to the child for each function in the render box
/// protocol. For example, a proxy box determines its size by asking its child
/// to layout with the same constraints and then matching the size.
///
/// A proxy box isn't useful on its own because you might as well just replace
/// the proxy box with its child. However, RenderProxyBox is a useful base class
/// for render objects that wish to mimic most, but not all, of the properties
/// of their child.
///
/// See also:
///
/// * [RenderProxySliver], a base class for render slivers that resemble their
/// children.
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {
/// Creates a proxy render box.
///
/// Proxy render boxes are rarely created directly because they simply proxy
/// the render box protocol to [child]. Instead, consider using one of the
/// subclasses.
RenderProxyBox([RenderBox? child]) {
this.child = child;
}
}
/// Implementation of [RenderProxyBox].
///
/// Use this mixin in situations where the proxying behavior
/// of [RenderProxyBox] is desired but inheriting from [RenderProxyBox] is
/// impractical (e.g. because you want to mix in other classes as well).
// TODO(ianh): Remove this class once https://github.com/dart-lang/sdk/issues/31543 is fixed
@optionalTypeArgs
mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChildMixin<T> {
@override
void setupParentData(RenderObject child) {
// We don't actually use the offset argument in BoxParentData, so let's
// avoid allocating it at all.
if (child.parentData is! ParentData) {
child.parentData = ParentData();
}
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null) {
return child!.getMinIntrinsicWidth(height);
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null) {
return child!.getMaxIntrinsicWidth(height);
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null) {
return child!.getMinIntrinsicHeight(width);
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null) {
return child!.getMaxIntrinsicHeight(width);
}
return 0.0;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
if (child != null) {
return child!.getDistanceToActualBaseline(baseline);
}
return super.computeDistanceToActualBaseline(baseline);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
if (child != null) {
return child!.getDryLayout(constraints);
}
return computeSizeForNoChild(constraints);
}
@override
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
/// Calculate the size the [RenderProxyBox] would have under the given
/// [BoxConstraints] for the case where it does not have a child.
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.smallest;
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return child?.hitTest(result, position: position) ?? false;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) { }
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.paintChild(child!, offset);
}
}
}
/// How to behave during hit tests.
enum HitTestBehavior {
/// Targets that defer to their children receive events within their bounds
/// only if one of their children is hit by the hit test.
deferToChild,
/// Opaque targets can be hit by hit tests, causing them to both receive
/// events within their bounds and prevent targets visually behind them from
/// also receiving events.
opaque,
/// Translucent targets both receive events within their bounds and permit
/// targets visually behind them to also receive events.
translucent,
}
/// A RenderProxyBox subclass that allows you to customize the
/// hit-testing behavior.
abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
/// Initializes member variables for subclasses.
///
/// By default, the [behavior] is [HitTestBehavior.deferToChild].
RenderProxyBoxWithHitTestBehavior({
this.behavior = HitTestBehavior.deferToChild,
RenderBox? child,
}) : super(child);
/// How to behave during hit testing.
HitTestBehavior behavior;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
bool hitTarget = false;
if (size.contains(position)) {
hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent) {
result.add(BoxHitTestEntry(this, position));
}
}
return hitTarget;
}
@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<HitTestBehavior>('behavior', behavior, defaultValue: null));
}
}
/// Imposes additional constraints on its child.
///
/// A render constrained box proxies most functions in the render box protocol
/// to its child, except that when laying out its child, it tightens the
/// constraints provided by its parent by enforcing the [additionalConstraints]
/// as well.
///
/// For example, if you wanted [child] to have a minimum height of 50.0 logical
/// pixels, you could use `const BoxConstraints(minHeight: 50.0)` as the
/// [additionalConstraints].
class RenderConstrainedBox extends RenderProxyBox {
/// Creates a render box that constrains its child.
///
/// The [additionalConstraints] argument must not be null and must be valid.
RenderConstrainedBox({
RenderBox? child,
required BoxConstraints additionalConstraints,
}) : assert(additionalConstraints != null),
assert(additionalConstraints.debugAssertIsValid()),
_additionalConstraints = additionalConstraints,
super(child);
/// Additional constraints to apply to [child] during layout.
BoxConstraints get additionalConstraints => _additionalConstraints;
BoxConstraints _additionalConstraints;
set additionalConstraints(BoxConstraints value) {
assert(value != null);
assert(value.debugAssertIsValid());
if (_additionalConstraints == value) {
return;
}
_additionalConstraints = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (_additionalConstraints.hasBoundedWidth && _additionalConstraints.hasTightWidth) {
return _additionalConstraints.minWidth;
}
final double width = super.computeMinIntrinsicWidth(height);
assert(width.isFinite);
if (!_additionalConstraints.hasInfiniteWidth) {
return _additionalConstraints.constrainWidth(width);
}
return width;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (_additionalConstraints.hasBoundedWidth && _additionalConstraints.hasTightWidth) {
return _additionalConstraints.minWidth;
}
final double width = super.computeMaxIntrinsicWidth(height);
assert(width.isFinite);
if (!_additionalConstraints.hasInfiniteWidth) {
return _additionalConstraints.constrainWidth(width);
}
return width;
}
@override
double computeMinIntrinsicHeight(double width) {
if (_additionalConstraints.hasBoundedHeight && _additionalConstraints.hasTightHeight) {
return _additionalConstraints.minHeight;
}
final double height = super.computeMinIntrinsicHeight(width);
assert(height.isFinite);
if (!_additionalConstraints.hasInfiniteHeight) {
return _additionalConstraints.constrainHeight(height);
}
return height;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (_additionalConstraints.hasBoundedHeight && _additionalConstraints.hasTightHeight) {
return _additionalConstraints.minHeight;
}
final double height = super.computeMaxIntrinsicHeight(width);
assert(height.isFinite);
if (!_additionalConstraints.hasInfiniteHeight) {
return _additionalConstraints.constrainHeight(height);
}
return height;
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
@override
Size computeDryLayout(BoxConstraints constraints) {
if (child != null) {
return child!.getDryLayout(_additionalConstraints.enforce(constraints));
} else {
return _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
final Paint paint;
if (child == null || child!.size.isEmpty) {
paint = Paint()
..color = const Color(0x90909090);
context.canvas.drawRect(offset & size, paint);
}
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<BoxConstraints>('additionalConstraints', additionalConstraints));
}
}
/// Constrains the child's [BoxConstraints.maxWidth] and
/// [BoxConstraints.maxHeight] if they're otherwise unconstrained.
///
/// This has the effect of giving the child a natural dimension in unbounded
/// environments. For example, by providing a [maxHeight] to a widget that
/// normally tries to be as big as possible, the widget will normally size
/// itself to fit its parent, but when placed in a vertical list, it will take
/// on the given height.
///
/// This is useful when composing widgets that normally try to match their
/// parents' size, so that they behave reasonably in lists (which are
/// unbounded).
class RenderLimitedBox extends RenderProxyBox {
/// Creates a render box that imposes a maximum width or maximum height on its
/// child if the child is otherwise unconstrained.
///
/// The [maxWidth] and [maxHeight] arguments not be null and must be
/// non-negative.
RenderLimitedBox({
RenderBox? child,
double maxWidth = double.infinity,
double maxHeight = double.infinity,
}) : assert(maxWidth != null && maxWidth >= 0.0),
assert(maxHeight != null && maxHeight >= 0.0),
_maxWidth = maxWidth,
_maxHeight = maxHeight,
super(child);
/// The value to use for maxWidth if the incoming maxWidth constraint is infinite.
double get maxWidth => _maxWidth;
double _maxWidth;
set maxWidth(double value) {
assert(value != null && value >= 0.0);
if (_maxWidth == value) {
return;
}
_maxWidth = value;
markNeedsLayout();
}
/// The value to use for maxHeight if the incoming maxHeight constraint is infinite.
double get maxHeight => _maxHeight;
double _maxHeight;
set maxHeight(double value) {
assert(value != null && value >= 0.0);
if (_maxHeight == value) {
return;
}
_maxHeight = value;
markNeedsLayout();
}
BoxConstraints _limitConstraints(BoxConstraints constraints) {
return BoxConstraints(
minWidth: constraints.minWidth,
maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth),
minHeight: constraints.minHeight,
maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : constraints.constrainHeight(maxHeight),
);
}
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild }) {
if (child != null) {
final Size childSize = layoutChild(child!, _limitConstraints(constraints));
return constraints.constrain(childSize);
}
return _limitConstraints(constraints).constrain(Size.zero);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.dryLayoutChild,
);
}
@override
void performLayout() {
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity));
properties.add(DoubleProperty('maxHeight', maxHeight, defaultValue: double.infinity));
}
}
/// Attempts to size the child to a specific aspect ratio.
///
/// The render object first tries the largest width permitted by the layout
/// constraints. The height of the render object is determined by applying the
/// given aspect ratio to the width, expressed as a ratio of width to height.
///
/// For example, a 16:9 width:height aspect ratio would have a value of
/// 16.0/9.0. If the maximum width is infinite, the initial width is determined
/// by applying the aspect ratio to the maximum height.
///
/// Now consider a second example, this time with an aspect ratio of 2.0 and
/// layout constraints that require the width to be between 0.0 and 100.0 and
/// the height to be between 0.0 and 100.0. We'll select a width of 100.0 (the
/// biggest allowed) and a height of 50.0 (to match the aspect ratio).
///
/// In that same situation, if the aspect ratio is 0.5, we'll also select a
/// width of 100.0 (still the biggest allowed) and we'll attempt to use a height
/// of 200.0. Unfortunately, that violates the constraints because the child can
/// be at most 100.0 pixels tall. The render object will then take that value
/// and apply the aspect ratio again to obtain a width of 50.0. That width is
/// permitted by the constraints and the child receives a width of 50.0 and a
/// height of 100.0. If the width were not permitted, the render object would
/// continue iterating through the constraints. If the render object does not
/// find a feasible size after consulting each constraint, the render object
/// will eventually select a size for the child that meets the layout
/// constraints but fails to meet the aspect ratio constraints.
class RenderAspectRatio extends RenderProxyBox {
/// Creates as render object with a specific aspect ratio.
///
/// The [aspectRatio] argument must be a finite, positive value.
RenderAspectRatio({
RenderBox? child,
required double aspectRatio,
}) : assert(aspectRatio != null),
assert(aspectRatio > 0.0),
assert(aspectRatio.isFinite),
_aspectRatio = aspectRatio,
super(child);
/// The aspect ratio to attempt to use.
///
/// The aspect ratio is expressed as a ratio of width to height. For example,
/// a 16:9 width:height aspect ratio would have a value of 16.0/9.0.
double get aspectRatio => _aspectRatio;
double _aspectRatio;
set aspectRatio(double value) {
assert(value != null);
assert(value > 0.0);
assert(value.isFinite);
if (_aspectRatio == value) {
return;
}
_aspectRatio = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (height.isFinite) {
return height * _aspectRatio;
}
if (child != null) {
return child!.getMinIntrinsicWidth(height);
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (height.isFinite) {
return height * _aspectRatio;
}
if (child != null) {
return child!.getMaxIntrinsicWidth(height);
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (width.isFinite) {
return width / _aspectRatio;
}
if (child != null) {
return child!.getMinIntrinsicHeight(width);
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (width.isFinite) {
return width / _aspectRatio;
}
if (child != null) {
return child!.getMaxIntrinsicHeight(width);
}
return 0.0;
}
Size _applyAspectRatio(BoxConstraints constraints) {
assert(constraints.debugAssertIsValid());
assert(() {
if (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight) {
throw FlutterError(
'$runtimeType has unbounded constraints.\n'
'This $runtimeType was given an aspect ratio of $aspectRatio but was given '
'both unbounded width and unbounded height constraints. Because both '
"constraints were unbounded, this render object doesn't know how much "
'size to consume.',
);
}
return true;
}());
if (constraints.isTight) {
return constraints.smallest;
}
double width = constraints.maxWidth;
double height;
// We default to picking the height based on the width, but if the width
// would be infinite, that's not sensible so we try to infer the height
// from the width.
if (width.isFinite) {
height = width / _aspectRatio;
} else {
height = constraints.maxHeight;
width = height * _aspectRatio;
}
// Similar to RenderImage, we iteratively attempt to fit within the given
// constraints while maintaining the given aspect ratio. The order of
// applying the constraints is also biased towards inferring the height
// from the width.
if (width > constraints.maxWidth) {
width = constraints.maxWidth;
height = width / _aspectRatio;
}
if (height > constraints.maxHeight) {
height = constraints.maxHeight;
width = height * _aspectRatio;
}
if (width < constraints.minWidth) {
width = constraints.minWidth;
height = width / _aspectRatio;
}
if (height < constraints.minHeight) {
height = constraints.minHeight;
width = height * _aspectRatio;
}
return constraints.constrain(Size(width, height));
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _applyAspectRatio(constraints);
}
@override
void performLayout() {
size = computeDryLayout(constraints);
if (child != null) {
child!.layout(BoxConstraints.tight(size));
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('aspectRatio', aspectRatio));
}
}
/// Sizes its child to the child's maximum intrinsic width.
///
/// This class is useful, for example, when unlimited width is available and
/// you would like a child that would otherwise attempt to expand infinitely to
/// instead size itself to a more reasonable width.
///
/// The constraints that this object passes to its child will adhere to the
/// parent's constraints, so if the constraints are not large enough to satisfy
/// the child's maximum intrinsic width, then the child will get less width
/// than it otherwise would. Likewise, if the minimum width constraint is
/// larger than the child's maximum intrinsic width, the child will be given
/// more width than it otherwise would.
///
/// If [stepWidth] is non-null, the child's width will be snapped to a multiple
/// of the [stepWidth]. Similarly, if [stepHeight] is non-null, the child's
/// height will be snapped to a multiple of the [stepHeight].
///
/// This class is relatively expensive, because it adds a speculative layout
/// pass before the final layout phase. Avoid using it where possible. In the
/// worst case, this render object can result in a layout that is O(N²) in the
/// depth of the tree.
///
/// See also:
///
/// * [Align], a widget that aligns its child within itself. This can be used
/// to loosen the constraints passed to the [RenderIntrinsicWidth],
/// allowing the [RenderIntrinsicWidth]'s child to be smaller than that of
/// its parent.
/// * [Row], which when used with [CrossAxisAlignment.stretch] can be used
/// to loosen just the width constraints that are passed to the
/// [RenderIntrinsicWidth], allowing the [RenderIntrinsicWidth]'s child's
/// width to be smaller than that of its parent.
class RenderIntrinsicWidth extends RenderProxyBox {
/// Creates a render object that sizes itself to its child's intrinsic width.
///
/// If [stepWidth] is non-null it must be > 0.0. Similarly If [stepHeight] is
/// non-null it must be > 0.0.
RenderIntrinsicWidth({
double? stepWidth,
double? stepHeight,
RenderBox? child,
}) : assert(stepWidth == null || stepWidth > 0.0),
assert(stepHeight == null || stepHeight > 0.0),
_stepWidth = stepWidth,
_stepHeight = stepHeight,
super(child);
/// If non-null, force the child's width to be a multiple of this value.
///
/// This value must be null or > 0.0.
double? get stepWidth => _stepWidth;
double? _stepWidth;
set stepWidth(double? value) {
assert(value == null || value > 0.0);
if (value == _stepWidth) {
return;
}
_stepWidth = value;
markNeedsLayout();
}
/// If non-null, force the child's height to be a multiple of this value.
///
/// This value must be null or > 0.0.
double? get stepHeight => _stepHeight;
double? _stepHeight;
set stepHeight(double? value) {
assert(value == null || value > 0.0);
if (value == _stepHeight) {
return;
}
_stepHeight = value;
markNeedsLayout();
}
static double _applyStep(double input, double? step) {
assert(input.isFinite);
if (step == null) {
return input;
}
return (input / step).ceil() * step;
}
@override
double computeMinIntrinsicWidth(double height) {
return computeMaxIntrinsicWidth(height);
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child == null) {
return 0.0;
}
final double width = child!.getMaxIntrinsicWidth(height);
return _applyStep(width, _stepWidth);
}
@override
double computeMinIntrinsicHeight(double width) {
if (child == null) {
return 0.0;
}
if (!width.isFinite) {
width = computeMaxIntrinsicWidth(double.infinity);
}
assert(width.isFinite);
final double height = child!.getMinIntrinsicHeight(width);
return _applyStep(height, _stepHeight);
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child == null) {
return 0.0;
}
if (!width.isFinite) {
width = computeMaxIntrinsicWidth(double.infinity);
}
assert(width.isFinite);
final double height = child!.getMaxIntrinsicHeight(width);
return _applyStep(height, _stepHeight);
}
Size _computeSize({required ChildLayouter layoutChild, required BoxConstraints constraints}) {
if (child != null) {
if (!constraints.hasTightWidth) {
final double width = child!.getMaxIntrinsicWidth(constraints.maxHeight);
assert(width.isFinite);
constraints = constraints.tighten(width: _applyStep(width, _stepWidth));
}
if (_stepHeight != null) {
final double height = child!.getMaxIntrinsicHeight(constraints.maxWidth);
assert(height.isFinite);
constraints = constraints.tighten(height: _applyStep(height, _stepHeight));
}
return layoutChild(child!, constraints);
} else {
return constraints.smallest;
}
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeSize(
layoutChild: ChildLayoutHelper.dryLayoutChild,
constraints: constraints,
);
}
@override
void performLayout() {
size = _computeSize(
layoutChild: ChildLayoutHelper.layoutChild,
constraints: constraints,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('stepWidth', stepWidth));
properties.add(DoubleProperty('stepHeight', stepHeight));
}
}
/// Sizes its child to the child's intrinsic height.
///
/// This class is useful, for example, when unlimited height is available and
/// you would like a child that would otherwise attempt to expand infinitely to
/// instead size itself to a more reasonable height.
///
/// The constraints that this object passes to its child will adhere to the
/// parent's constraints, so if the constraints are not large enough to satisfy
/// the child's maximum intrinsic height, then the child will get less height
/// than it otherwise would. Likewise, if the minimum height constraint is
/// larger than the child's maximum intrinsic height, the child will be given
/// more height than it otherwise would.
///
/// This class is relatively expensive, because it adds a speculative layout
/// pass before the final layout phase. Avoid using it where possible. In the
/// worst case, this render object can result in a layout that is O(N²) in the
/// depth of the tree.
///
/// See also:
///
/// * [Align], a widget that aligns its child within itself. This can be used
/// to loosen the constraints passed to the [RenderIntrinsicHeight],
/// allowing the [RenderIntrinsicHeight]'s child to be smaller than that of
/// its parent.
/// * [Column], which when used with [CrossAxisAlignment.stretch] can be used
/// to loosen just the height constraints that are passed to the
/// [RenderIntrinsicHeight], allowing the [RenderIntrinsicHeight]'s child's
/// height to be smaller than that of its parent.
class RenderIntrinsicHeight extends RenderProxyBox {
/// Creates a render object that sizes itself to its child's intrinsic height.
RenderIntrinsicHeight({
RenderBox? child,
}) : super(child);
@override
double computeMinIntrinsicWidth(double height) {
if (child == null) {
return 0.0;
}
if (!height.isFinite) {
height = child!.getMaxIntrinsicHeight(double.infinity);
}
assert(height.isFinite);
return child!.getMinIntrinsicWidth(height);
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child == null) {
return 0.0;
}
if (!height.isFinite) {
height = child!.getMaxIntrinsicHeight(double.infinity);
}
assert(height.isFinite);
return child!.getMaxIntrinsicWidth(height);
}
@override
double computeMinIntrinsicHeight(double width) {
return computeMaxIntrinsicHeight(width);
}
Size _computeSize({required ChildLayouter layoutChild, required BoxConstraints constraints}) {
if (child != null) {
if (!constraints.hasTightHeight) {
final double height = child!.getMaxIntrinsicHeight(constraints.maxWidth);
assert(height.isFinite);
constraints = constraints.tighten(height: height);
}
return layoutChild(child!, constraints);
} else {
return constraints.smallest;
}
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeSize(
layoutChild: ChildLayoutHelper.dryLayoutChild,
constraints: constraints,
);
}
@override
void performLayout() {
size = _computeSize(
layoutChild: ChildLayoutHelper.layoutChild,
constraints: constraints,
);
}
}
/// Makes its child partially transparent.
///
/// This class paints its child into an intermediate buffer and then blends the
/// child back into the scene partially transparent.
///
/// For values of opacity other than 0.0 and 1.0, this class is relatively
/// expensive because it requires painting the child into an intermediate
/// buffer. For the value 0.0, the child is simply not painted at all. For the
/// value 1.0, the child is painted immediately without an intermediate buffer.
class RenderOpacity extends RenderProxyBox {
/// Creates a partially transparent render object.
///
/// The [opacity] argument must be between 0.0 and 1.0, inclusive.
RenderOpacity({
double opacity = 1.0,
bool alwaysIncludeSemantics = false,
RenderBox? child,
}) : assert(opacity != null),
assert(opacity >= 0.0 && opacity <= 1.0),
assert(alwaysIncludeSemantics != null),
_opacity = opacity,
_alwaysIncludeSemantics = alwaysIncludeSemantics,
_alpha = ui.Color.getAlphaFromOpacity(opacity),
super(child);
@override
bool get alwaysNeedsCompositing => child != null && _alpha > 0;
int _alpha;
/// The fraction to scale the child's alpha value.
///
/// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent
/// (i.e., invisible).
///
/// The opacity must not be null.
///
/// Values 1.0 and 0.0 are painted with a fast path. Other values
/// require painting the child into an intermediate buffer, which is
/// expensive.
double get opacity => _opacity;
double _opacity;
set opacity(double value) {
assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (_opacity == value) {
return;
}
final bool didNeedCompositing = alwaysNeedsCompositing;
final bool wasVisible = _alpha != 0;
_opacity = value;
_alpha = ui.Color.getAlphaFromOpacity(_opacity);
if (didNeedCompositing != alwaysNeedsCompositing) {
markNeedsCompositingBitsUpdate();
}
markNeedsPaint();
if (wasVisible != (_alpha != 0) && !alwaysIncludeSemantics) {
markNeedsSemanticsUpdate();
}
}
/// Whether child semantics are included regardless of the opacity.
///
/// If false, semantics are excluded when [opacity] is 0.0.
///
/// Defaults to false.
bool get alwaysIncludeSemantics => _alwaysIncludeSemantics;
bool _alwaysIncludeSemantics;
set alwaysIncludeSemantics(bool value) {
if (value == _alwaysIncludeSemantics) {
return;
}
_alwaysIncludeSemantics = value;
markNeedsSemanticsUpdate();
}
@override
bool paintsChild(RenderBox child) {
assert(child.parent == this);
return _alpha > 0;
}
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) {
return;
}
if (_alpha == 0) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
return;
}
assert(needsCompositing);
layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && (_alpha != 0 || alwaysIncludeSemantics)) {
visitor(child!);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('opacity', opacity));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
/// Implementation of [RenderAnimatedOpacity] and [RenderSliverAnimatedOpacity].
///
/// This mixin allows the logic of animating opacity to be used with different
/// layout models, e.g. the way that [RenderAnimatedOpacity] uses it for [RenderBox]
/// and [RenderSliverAnimatedOpacity] uses it for [RenderSliver].
mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChildMixin<T> {
int? _alpha;
@override
bool get isRepaintBoundary => child != null && _currentlyIsRepaintBoundary!;
bool? _currentlyIsRepaintBoundary;
@override
OffsetLayer updateCompositedLayer({required covariant OpacityLayer? oldLayer}) {
final OpacityLayer updatedLayer = oldLayer ?? OpacityLayer();
updatedLayer.alpha = _alpha;
return updatedLayer;
}
/// The animation that drives this render object's opacity.
///
/// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent
/// (i.e., invisible).
///
/// To change the opacity of a child in a static manner, not animated,
/// consider [RenderOpacity] instead.
///
/// This getter cannot be read until the value has been set. It should be set
/// by the constructor of the class in which this mixin is included.
Animation<double> get opacity => _opacity!;
Animation<double>? _opacity;
set opacity(Animation<double> value) {
assert(value != null);
if (_opacity == value) {
return;
}
if (attached && _opacity != null) {
opacity.removeListener(_updateOpacity);
}
_opacity = value;
if (attached) {
opacity.addListener(_updateOpacity);
}
_updateOpacity();
}
/// Whether child semantics are included regardless of the opacity.
///
/// If false, semantics are excluded when [opacity] is 0.0.
///
/// Defaults to false.
///
/// This getter cannot be read until the value has been set. It should be set
/// by the constructor of the class in which this mixin is included.
bool get alwaysIncludeSemantics => _alwaysIncludeSemantics!;
bool? _alwaysIncludeSemantics;
set alwaysIncludeSemantics(bool value) {
if (value == _alwaysIncludeSemantics) {
return;
}
_alwaysIncludeSemantics = value;
markNeedsSemanticsUpdate();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
opacity.addListener(_updateOpacity);
_updateOpacity(); // in case it changed while we weren't listening
}
@override
void detach() {
opacity.removeListener(_updateOpacity);
super.detach();
}
void _updateOpacity() {
final int? oldAlpha = _alpha;
_alpha = ui.Color.getAlphaFromOpacity(opacity.value);
if (oldAlpha != _alpha) {
final bool? wasRepaintBoundary = _currentlyIsRepaintBoundary;
_currentlyIsRepaintBoundary = _alpha! > 0;
if (child != null && wasRepaintBoundary != _currentlyIsRepaintBoundary) {
markNeedsCompositingBitsUpdate();
}
markNeedsCompositedLayerUpdate();
if (oldAlpha == 0 || _alpha == 0) {
markNeedsSemanticsUpdate();
}
}
}
@override
bool paintsChild(RenderObject child) {
assert(child.parent == this);
return opacity.value > 0;
}
@override
void paint(PaintingContext context, Offset offset) {
if (_alpha == 0) {
return;
}
super.paint(context, offset);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && (_alpha != 0 || alwaysIncludeSemantics)) {
visitor(child!);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>('opacity', opacity));
properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
/// Makes its child partially transparent, driven from an [Animation].
///
/// This is a variant of [RenderOpacity] that uses an [Animation<double>] rather
/// than a [double] to control the opacity.
class RenderAnimatedOpacity extends RenderProxyBox with RenderProxyBoxMixin, RenderAnimatedOpacityMixin<RenderBox> {
/// Creates a partially transparent render object.
///
/// The [opacity] argument must not be null.
RenderAnimatedOpacity({
required Animation<double> opacity,
bool alwaysIncludeSemantics = false,
RenderBox? child,
}) : assert(opacity != null),
assert(alwaysIncludeSemantics != null),
super(child) {
this.opacity = opacity;
this.alwaysIncludeSemantics = alwaysIncludeSemantics;
}
}
/// Signature for a function that creates a [Shader] for a given [Rect].
///
/// Used by [RenderShaderMask] and the [ShaderMask] widget.
typedef ShaderCallback = Shader Function(Rect bounds);
/// Applies a mask generated by a [Shader] to its child.
///
/// For example, [RenderShaderMask] can be used to gradually fade out the edge
/// of a child by using a [ui.Gradient.linear] mask.
class RenderShaderMask extends RenderProxyBox {
/// Creates a render object that applies a mask generated by a [Shader] to its child.
///
/// The [shaderCallback] and [blendMode] arguments must not be null.
RenderShaderMask({
RenderBox? child,
required ShaderCallback shaderCallback,
BlendMode blendMode = BlendMode.modulate,
}) : assert(shaderCallback != null),
assert(blendMode != null),
_shaderCallback = shaderCallback,
_blendMode = blendMode,
super(child);
@override
ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;
/// Called to creates the [Shader] that generates the mask.
///
/// The shader callback is called with the current size of the child so that
/// it can customize the shader to the size and location of the child.
///
/// The rectangle will always be at the origin when called by
/// [RenderShaderMask].
// TODO(abarth): Use the delegate pattern here to avoid generating spurious
// repaints when the ShaderCallback changes identity.
ShaderCallback get shaderCallback => _shaderCallback;
ShaderCallback _shaderCallback;
set shaderCallback(ShaderCallback value) {
assert(value != null);
if (_shaderCallback == value) {
return;
}
_shaderCallback = value;
markNeedsPaint();
}
/// The [BlendMode] to use when applying the shader to the child.
///
/// The default, [BlendMode.modulate], is useful for applying an alpha blend
/// to the child. Other blend modes can be used to create other effects.
BlendMode get blendMode => _blendMode;
BlendMode _blendMode;
set blendMode(BlendMode value) {
assert(value != null);
if (_blendMode == value) {
return;
}
_blendMode = value;
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => child != null;
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
layer ??= ShaderMaskLayer();
layer!
..shader = _shaderCallback(Offset.zero & size)
..maskRect = offset & size
..blendMode = _blendMode;
context.pushLayer(layer!, super.paint, offset);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
} else {
layer = null;
}
}
}
/// Applies a filter to the existing painted content and then paints [child].
///
/// This effect is relatively expensive, especially if the filter is non-local,
/// such as a blur.
class RenderBackdropFilter extends RenderProxyBox {
/// Creates a backdrop filter.
///
/// The [filter] argument must not be null.
/// The [blendMode] argument, if provided, must not be null
/// and will default to [BlendMode.srcOver].
RenderBackdropFilter({ RenderBox? child, required ui.ImageFilter filter, BlendMode blendMode = BlendMode.srcOver })
: assert(filter != null),
assert(blendMode != null),
_filter = filter,
_blendMode = blendMode,
super(child);
@override
BackdropFilterLayer? get layer => super.layer as BackdropFilterLayer?;
/// The image filter to apply to the existing painted content before painting
/// the child.
///
/// For example, consider using [ui.ImageFilter.blur] to create a backdrop
/// blur effect.
ui.ImageFilter get filter => _filter;
ui.ImageFilter _filter;
set filter(ui.ImageFilter value) {
assert(value != null);
if (_filter == value) {
return;
}
_filter = value;
markNeedsPaint();
}
/// The blend mode to use to apply the filtered background content onto the background
/// surface.
///
/// {@macro flutter.widgets.BackdropFilter.blendMode}
BlendMode get blendMode => _blendMode;
BlendMode _blendMode;
set blendMode(BlendMode value) {
assert(value != null);
if (_blendMode == value) {
return;
}
_blendMode = value;
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => child != null;
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
layer ??= BackdropFilterLayer();
layer!.filter = _filter;
layer!.blendMode = _blendMode;
context.pushLayer(layer!, super.paint, offset);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
} else {
layer = null;
}
}
}
/// An interface for providing custom clips.
///
/// This class is used by a number of clip widgets (e.g., [ClipRect] and
/// [ClipPath]).
///
/// The [getClip] method is called whenever the custom clip needs to be updated.
///
/// The [shouldReclip] method is called when a new instance of the class
/// is provided, to check if the new instance actually represents different
/// information.
///
/// The most efficient way to update the clip provided by this class is to
/// supply a `reclip` argument to the constructor of the [CustomClipper]. The
/// custom object will listen to this animation and update the clip whenever the
/// animation ticks, avoiding both the build and layout phases of the pipeline.
///
/// See also:
///
/// * [ClipRect], which can be customized with a [CustomClipper<Rect>].
/// * [ClipRRect], which can be customized with a [CustomClipper<RRect>].
/// * [ClipOval], which can be customized with a [CustomClipper<Rect>].
/// * [ClipPath], which can be customized with a [CustomClipper<Path>].
/// * [ShapeBorderClipper], for specifying a clip path using a [ShapeBorder].
abstract class CustomClipper<T> extends Listenable {
/// Creates a custom clipper.
///
/// The clipper will update its clip whenever [reclip] notifies its listeners.
const CustomClipper({ Listenable? reclip }) : _reclip = reclip;
final Listenable? _reclip;
/// Register a closure to be notified when it is time to reclip.
///
/// The [CustomClipper] implementation merely forwards to the same method on
/// the [Listenable] provided to the constructor in the `reclip` argument, if
/// it was not null.
@override
void addListener(VoidCallback listener) => _reclip?.addListener(listener);
/// Remove a previously registered closure from the list of closures that the
/// object notifies when it is time to reclip.
///
/// The [CustomClipper] implementation merely forwards to the same method on
/// the [Listenable] provided to the constructor in the `reclip` argument, if
/// it was not null.
@override
void removeListener(VoidCallback listener) => _reclip?.removeListener(listener);
/// Returns a description of the clip given that the render object being
/// clipped is of the given size.
T getClip(Size size);
/// Returns an approximation of the clip returned by [getClip], as
/// an axis-aligned Rect. This is used by the semantics layer to
/// determine whether widgets should be excluded.
///
/// By default, this returns a rectangle that is the same size as
/// the RenderObject. If getClip returns a shape that is roughly the
/// same size as the RenderObject (e.g. it's a rounded rectangle
/// with very small arcs in the corners), then this may be adequate.
Rect getApproximateClipRect(Size size) => Offset.zero & size;
/// Called whenever a new instance of the custom clipper delegate class is
/// provided to the clip object, or any time that a new clip object is created
/// with a new instance of the custom clipper delegate class (which amounts to
/// the same thing, because the latter is implemented in terms of the former).
///
/// If the new instance represents different information than the old
/// instance, then the method should return true, otherwise it should return
/// false.
///
/// If the method returns false, then the [getClip] call might be optimized
/// away.
///
/// It's possible that the [getClip] method will get called even if
/// [shouldReclip] returns false or if the [shouldReclip] method is never
/// called at all (e.g. if the box changes size).
bool shouldReclip(covariant CustomClipper<T> oldClipper);
@override
String toString() => objectRuntimeType(this, 'CustomClipper');
}
/// A [CustomClipper] that clips to the outer path of a [ShapeBorder].
class ShapeBorderClipper extends CustomClipper<Path> {
/// Creates a [ShapeBorder] clipper.
///
/// The [shape] argument must not be null.
///
/// The [textDirection] argument must be provided non-null if [shape]
/// has a text direction dependency (for example if it is expressed in terms
/// of "start" and "end" instead of "left" and "right"). It may be null if
/// the border will not need the text direction to paint itself.
const ShapeBorderClipper({
required this.shape,
this.textDirection,
}) : assert(shape != null);
/// The shape border whose outer path this clipper clips to.
final ShapeBorder shape;
/// The text direction to use for getting the outer path for [shape].
///
/// [ShapeBorder]s can depend on the text direction (e.g having a "dent"
/// towards the start of the shape).
final TextDirection? textDirection;
/// Returns the outer path of [shape] as the clip.
@override
Path getClip(Size size) {
return shape.getOuterPath(Offset.zero & size, textDirection: textDirection);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
if (oldClipper.runtimeType != ShapeBorderClipper) {
return true;
}
final ShapeBorderClipper typedOldClipper = oldClipper as ShapeBorderClipper;
return typedOldClipper.shape != shape
|| typedOldClipper.textDirection != textDirection;
}
}
abstract class _RenderCustomClip<T> extends RenderProxyBox {
_RenderCustomClip({
RenderBox? child,
CustomClipper<T>? clipper,
Clip clipBehavior = Clip.antiAlias,
}) : assert(clipBehavior != null),
_clipper = clipper,
_clipBehavior = clipBehavior,
super(child);
/// If non-null, determines which clip to use on the child.
CustomClipper<T>? get clipper => _clipper;
CustomClipper<T>? _clipper;
set clipper(CustomClipper<T>? newClipper) {
if (_clipper == newClipper) {
return;
}
final CustomClipper<T>? oldClipper = _clipper;
_clipper = newClipper;
assert(newClipper != null || oldClipper != null);
if (newClipper == null || oldClipper == null ||
newClipper.runtimeType != oldClipper.runtimeType ||
newClipper.shouldReclip(oldClipper)) {
_markNeedsClip();
}
if (attached) {
oldClipper?.removeListener(_markNeedsClip);
newClipper?.addListener(_markNeedsClip);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_clipper?.addListener(_markNeedsClip);
}
@override
void detach() {
_clipper?.removeListener(_markNeedsClip);
super.detach();
}
void _markNeedsClip() {
_clip = null;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
T get _defaultClip;
T? _clip;
Clip get clipBehavior => _clipBehavior;
set clipBehavior(Clip value) {
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
}
}
Clip _clipBehavior;
@override
void performLayout() {
final Size? oldSize = hasSize ? size : null;
super.performLayout();
if (oldSize != size) {
_clip = null;
}
}
void _updateClip() {
_clip ??= _clipper?.getClip(size) ?? _defaultClip;
}
@override
Rect? describeApproximatePaintClip(RenderObject child) {
switch (clipBehavior) {
case Clip.none:
return null;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
return _clipper?.getApproximateClipRect(size) ?? Offset.zero & size;
}
}
Paint? _debugPaint;
TextPainter? _debugText;
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
_debugPaint ??= Paint()
..shader = ui.Gradient.linear(
Offset.zero,
const Offset(10.0, 10.0),
<Color>[const Color(0x00000000), const Color(0xFFFF00FF), const Color(0xFFFF00FF), const Color(0x00000000)],
<double>[0.25, 0.25, 0.75, 0.75],
TileMode.repeated,
)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
_debugText ??= TextPainter(
text: const TextSpan(
text: '',
style: TextStyle(
color: Color(0xFFFF00FF),
fontSize: 14.0,
),
),
textDirection: TextDirection.rtl, // doesn't matter, it's one character
)
..layout();
return true;
}());
}
@override
void dispose() {
_debugText?.dispose();
_debugText = null;
super.dispose();
}
}
/// Clips its child using a rectangle.
///
/// By default, [RenderClipRect] prevents its child from painting outside its
/// bounds, but the size and location of the clip rect can be customized using a
/// custom [clipper].
class RenderClipRect extends _RenderCustomClip<Rect> {
/// Creates a rectangular clip.
///
/// If [clipper] is null, the clip will match the layout size and position of
/// the child.
///
/// The [clipBehavior] must not be null. If [clipBehavior] is
/// [Clip.none], no clipping will be applied.
RenderClipRect({
super.child,
super.clipper,
super.clipBehavior,
}) : assert(clipBehavior != null);
@override
Rect get _defaultClip => Offset.zero & size;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip!.contains(position)) {
return false;
}
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (clipBehavior != Clip.none) {
_updateClip();
layer = context.pushClipRect(
needsCompositing,
offset,
_clip!,
super.paint,
clipBehavior: clipBehavior,
oldLayer: layer as ClipRectLayer?,
);
} else {
context.paintChild(child!, offset);
layer = null;
}
} else {
layer = null;
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
if (clipBehavior != Clip.none) {
context.canvas.drawRect(_clip!.shift(offset), _debugPaint!);
_debugText!.paint(context.canvas, offset + Offset(_clip!.width / 8.0, -_debugText!.text!.style!.fontSize! * 1.1));
}
}
return true;
}());
}
}
/// Clips its child using a rounded rectangle.
///
/// By default, [RenderClipRRect] uses its own bounds as the base rectangle for
/// the clip, but the size and location of the clip can be customized using a
/// custom [clipper].
class RenderClipRRect extends _RenderCustomClip<RRect> {
/// Creates a rounded-rectangular clip.
///
/// The [borderRadius] defaults to [BorderRadius.zero], i.e. a rectangle with
/// right-angled corners.
///
/// If [clipper] is non-null, then [borderRadius] is ignored.
///
/// The [clipBehavior] argument must not be null. If [clipBehavior] is
/// [Clip.none], no clipping will be applied.
RenderClipRRect({
super.child,
BorderRadiusGeometry borderRadius = BorderRadius.zero,
CustomClipper<RRect>? clipper,
super.clipBehavior,
TextDirection? textDirection,
}) : assert(clipBehavior != null),
_borderRadius = borderRadius,
_textDirection = textDirection,
super(clipper: clipper) {
assert(_borderRadius != null || clipper != null);
}
/// The border radius of the rounded corners.
///
/// Values are clamped so that horizontal and vertical radii sums do not
/// exceed width/height.
///
/// This value is ignored if [clipper] is non-null.
BorderRadiusGeometry get borderRadius => _borderRadius;
BorderRadiusGeometry _borderRadius;
set borderRadius(BorderRadiusGeometry value) {
assert(value != null);
if (_borderRadius == value) {
return;
}
_borderRadius = value;
_markNeedsClip();
}
/// The text direction with which to resolve [borderRadius].
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
_markNeedsClip();
}
@override
RRect get _defaultClip => _borderRadius.resolve(textDirection).toRRect(Offset.zero & size);
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip!.contains(position)) {
return false;
}
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (clipBehavior != Clip.none) {
_updateClip();
layer = context.pushClipRRect(
needsCompositing,
offset,
_clip!.outerRect,
_clip!,
super.paint,
clipBehavior: clipBehavior,
oldLayer: layer as ClipRRectLayer?,
);
} else {
context.paintChild(child!, offset);
layer = null;
}
} else {
layer = null;
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
if (clipBehavior != Clip.none) {
context.canvas.drawRRect(_clip!.shift(offset), _debugPaint!);
_debugText!.paint(context.canvas, offset + Offset(_clip!.tlRadiusX, -_debugText!.text!.style!.fontSize! * 1.1));
}
}
return true;
}());
}
}
/// Clips its child using an oval.
///
/// By default, inscribes an axis-aligned oval into its layout dimensions and
/// prevents its child from painting outside that oval, but the size and
/// location of the clip oval can be customized using a custom [clipper].
class RenderClipOval extends _RenderCustomClip<Rect> {
/// Creates an oval-shaped clip.
///
/// If [clipper] is null, the oval will be inscribed into the layout size and
/// position of the child.
///
/// The [clipBehavior] argument must not be null. If [clipBehavior] is
/// [Clip.none], no clipping will be applied.
RenderClipOval({
super.child,
super.clipper,
super.clipBehavior,
}) : assert(clipBehavior != null);
Rect? _cachedRect;
late Path _cachedPath;
Path _getClipPath(Rect rect) {
if (rect != _cachedRect) {
_cachedRect = rect;
_cachedPath = Path()..addOval(_cachedRect!);
}
return _cachedPath;
}
@override
Rect get _defaultClip => Offset.zero & size;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
_updateClip();
assert(_clip != null);
final Offset center = _clip!.center;
// convert the position to an offset from the center of the unit circle
final Offset offset = Offset(
(position.dx - center.dx) / _clip!.width,
(position.dy - center.dy) / _clip!.height,
);
// check if the point is outside the unit circle
if (offset.distanceSquared > 0.25) { // x^2 + y^2 > r^2
return false;
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (clipBehavior != Clip.none) {
_updateClip();
layer = context.pushClipPath(
needsCompositing,
offset,
_clip!,
_getClipPath(_clip!),
super.paint,
clipBehavior: clipBehavior,
oldLayer: layer as ClipPathLayer?,
);
} else {
context.paintChild(child!, offset);
layer = null;
}
} else {
layer = null;
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
if (clipBehavior != Clip.none) {
context.canvas.drawPath(_getClipPath(_clip!).shift(offset), _debugPaint!);
_debugText!.paint(context.canvas, offset + Offset((_clip!.width - _debugText!.width) / 2.0, -_debugText!.text!.style!.fontSize! * 1.1));
}
}
return true;
}());
}
}
/// Clips its child using a path.
///
/// Takes a delegate whose primary method returns a path that should
/// be used to prevent the child from painting outside the path.
///
/// Clipping to a path is expensive. Certain shapes have more
/// optimized render objects:
///
/// * To clip to a rectangle, consider [RenderClipRect].
/// * To clip to an oval or circle, consider [RenderClipOval].
/// * To clip to a rounded rectangle, consider [RenderClipRRect].
class RenderClipPath extends _RenderCustomClip<Path> {
/// Creates a path clip.
///
/// If [clipper] is null, the clip will be a rectangle that matches the layout
/// size and location of the child. However, rather than use this default,
/// consider using a [RenderClipRect], which can achieve the same effect more
/// efficiently.
///
/// The [clipBehavior] argument must not be null. If [clipBehavior] is
/// [Clip.none], no clipping will be applied.
RenderClipPath({
super.child,
super.clipper,
super.clipBehavior,
}) : assert(clipBehavior != null);
@override
Path get _defaultClip => Path()..addRect(Offset.zero & size);
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip!.contains(position)) {
return false;
}
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (clipBehavior != Clip.none) {
_updateClip();
layer = context.pushClipPath(
needsCompositing,
offset,
Offset.zero & size,
_clip!,
super.paint,
clipBehavior: clipBehavior,
oldLayer: layer as ClipPathLayer?,
);
} else {
context.paintChild(child!, offset);
layer = null;
}
} else {
layer = null;
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
if (clipBehavior != Clip.none) {
context.canvas.drawPath(_clip!.shift(offset), _debugPaint!);
_debugText!.paint(context.canvas, offset);
}
}
return true;
}());
}
}
/// A physical model layer casts a shadow based on its [elevation].
///
/// The concrete implementations [RenderPhysicalModel] and [RenderPhysicalShape]
/// determine the actual shape of the physical model.
abstract class _RenderPhysicalModelBase<T> extends _RenderCustomClip<T> {
/// The [shape], [elevation], [color], and [shadowColor] must not be null.
/// Additionally, the [elevation] must be non-negative.
_RenderPhysicalModelBase({
required super.child,
required double elevation,
required Color color,
required Color shadowColor,
super.clipBehavior = Clip.none,
super.clipper,
}) : assert(elevation != null && elevation >= 0.0),
assert(color != null),
assert(shadowColor != null),
assert(clipBehavior != null),
_elevation = elevation,
_color = color,
_shadowColor = shadowColor;
/// The z-coordinate relative to the parent at which to place this material.
///
/// The value is non-negative.
///
/// If [debugDisableShadows] is set, this value is ignored and no shadow is
/// drawn (an outline is rendered instead).
double get elevation => _elevation;
double _elevation;
set elevation(double value) {
assert(value != null && value >= 0.0);
if (elevation == value) {
return;
}
final bool didNeedCompositing = alwaysNeedsCompositing;
_elevation = value;
if (didNeedCompositing != alwaysNeedsCompositing) {
markNeedsCompositingBitsUpdate();
}
markNeedsPaint();
}
/// The shadow color.
Color get shadowColor => _shadowColor;
Color _shadowColor;
set shadowColor(Color value) {
assert(value != null);
if (shadowColor == value) {
return;
}
_shadowColor = value;
markNeedsPaint();
}
/// The background color.
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (color == value) {
return;
}
_color = value;
markNeedsPaint();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.elevation = elevation;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DoubleProperty('elevation', elevation));
description.add(ColorProperty('color', color));
description.add(ColorProperty('shadowColor', color));
}
}
final Paint _transparentPaint = Paint()..color = const Color(0x00000000);
/// Creates a physical model layer that clips its child to a rounded
/// rectangle.
///
/// A physical model layer casts a shadow based on its [elevation].
class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
/// Creates a rounded-rectangular clip.
///
/// The [color] is required.
///
/// The [shape], [elevation], [color], [clipBehavior], and [shadowColor]
/// arguments must not be null. Additionally, the [elevation] must be
/// non-negative.
RenderPhysicalModel({
super.child,
BoxShape shape = BoxShape.rectangle,
super.clipBehavior,
BorderRadius? borderRadius,
super.elevation = 0.0,
required super.color,
super.shadowColor = const Color(0xFF000000),
}) : assert(shape != null),
assert(clipBehavior != null),
assert(elevation != null && elevation >= 0.0),
assert(color != null),
assert(shadowColor != null),
_shape = shape,
_borderRadius = borderRadius;
/// The shape of the layer.
///
/// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners
/// of the rectangle.
BoxShape get shape => _shape;
BoxShape _shape;
set shape(BoxShape value) {
assert(value != null);
if (shape == value) {
return;
}
_shape = value;
_markNeedsClip();
}
/// The border radius of the rounded corners.
///
/// Values are clamped so that horizontal and vertical radii sums do not
/// exceed width/height.
///
/// This property is ignored if the [shape] is not [BoxShape.rectangle].
///
/// The value null is treated like [BorderRadius.zero].
BorderRadius? get borderRadius => _borderRadius;
BorderRadius? _borderRadius;
set borderRadius(BorderRadius? value) {
if (borderRadius == value) {
return;
}
_borderRadius = value;
_markNeedsClip();
}
@override
RRect get _defaultClip {
assert(hasSize);
assert(_shape != null);
final Rect rect = Offset.zero & size;
switch (_shape) {
case BoxShape.rectangle:
return (borderRadius ?? BorderRadius.zero).toRRect(rect);
case BoxShape.circle:
return RRect.fromRectXY(rect, rect.width / 2, rect.height / 2);
}
}
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip!.contains(position)) {
return false;
}
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) {
layer = null;
return;
}
_updateClip();
final RRect offsetRRect = _clip!.shift(offset);
final Rect offsetBounds = offsetRRect.outerRect;
final Path offsetRRectAsPath = Path()..addRRect(offsetRRect);
bool paintShadows = true;
assert(() {
if (debugDisableShadows) {
if (elevation > 0.0) {
context.canvas.drawRRect(
offsetRRect,
Paint()
..color = shadowColor
..style = PaintingStyle.stroke
..strokeWidth = elevation * 2.0,
);
}
paintShadows = false;
}
return true;
}());
final Canvas canvas = context.canvas;
if (elevation != 0.0 && paintShadows) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
// TODO(jsimmons): remove this when Skia does it for us.
canvas.drawRect(
offsetBounds.inflate(20.0),
_transparentPaint,
);
canvas.drawShadow(
offsetRRectAsPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
final bool usesSaveLayer = clipBehavior == Clip.antiAliasWithSaveLayer;
if (!usesSaveLayer) {
canvas.drawRRect(
offsetRRect,
Paint()..color = color
);
}
layer = context.pushClipRRect(
needsCompositing,
offset,
Offset.zero & size,
_clip!,
(PaintingContext context, Offset offset) {
if (usesSaveLayer) {
// If we want to avoid the bleeding edge artifact
// (https://github.com/flutter/flutter/issues/18057#issue-328003931)
// using saveLayer, we have to call drawPaint instead of drawPath as
// anti-aliased drawPath will always have such artifacts.
context.canvas.drawPaint( Paint()..color = color);
}
super.paint(context, offset);
},
oldLayer: layer as ClipRRectLayer?,
clipBehavior: clipBehavior,
);
assert(() {
layer?.debugCreator = debugCreator;
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DiagnosticsProperty<BoxShape>('shape', shape));
description.add(DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius));
}
}
/// Creates a physical shape layer that clips its child to a [Path].
///
/// A physical shape layer casts a shadow based on its [elevation].
///
/// See also:
///
/// * [RenderPhysicalModel], which is optimized for rounded rectangles and
/// circles.
class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
/// Creates an arbitrary shape clip.
///
/// The [color] and [clipper] parameters are required.
///
/// The [clipper], [elevation], [color] and [shadowColor] must not be null.
/// Additionally, the [elevation] must be non-negative.
RenderPhysicalShape({
super.child,
required CustomClipper<Path> super.clipper,
super.clipBehavior,
super.elevation = 0.0,
required super.color,
super.shadowColor = const Color(0xFF000000),
}) : assert(clipper != null),
assert(elevation != null && elevation >= 0.0),
assert(color != null),
assert(shadowColor != null);
@override
Path get _defaultClip => Path()..addRect(Offset.zero & size);
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip!.contains(position)) {
return false;
}
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) {
layer = null;
return;
}
_updateClip();
final Rect offsetBounds = offset & size;
final Path offsetPath = _clip!.shift(offset);
bool paintShadows = true;
assert(() {
if (debugDisableShadows) {
if (elevation > 0.0) {
context.canvas.drawPath(
offsetPath,
Paint()
..color = shadowColor
..style = PaintingStyle.stroke
..strokeWidth = elevation * 2.0,
);
}
paintShadows = false;
}
return true;
}());
final Canvas canvas = context.canvas;
if (elevation != 0.0 && paintShadows) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
// TODO(jsimmons): remove this when Skia does it for us.
canvas.drawRect(
offsetBounds.inflate(20.0),
_transparentPaint,
);
canvas.drawShadow(
offsetPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
final bool usesSaveLayer = clipBehavior == Clip.antiAliasWithSaveLayer;
if (!usesSaveLayer) {
canvas.drawPath(
offsetPath,
Paint()..color = color
);
}
layer = context.pushClipPath(
needsCompositing,
offset,
Offset.zero & size,
_clip!,
(PaintingContext context, Offset offset) {
if (usesSaveLayer) {
// If we want to avoid the bleeding edge artifact
// (https://github.com/flutter/flutter/issues/18057#issue-328003931)
// using saveLayer, we have to call drawPaint instead of drawPath as
// anti-aliased drawPath will always have such artifacts.
context.canvas.drawPaint( Paint()..color = color);
}
super.paint(context, offset);
},
oldLayer: layer as ClipPathLayer?,
clipBehavior: clipBehavior,
);
assert(() {
layer?.debugCreator = debugCreator;
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DiagnosticsProperty<CustomClipper<Path>>('clipper', clipper));
}
}
/// Where to paint a box decoration.
enum DecorationPosition {
/// Paint the box decoration behind the children.
background,
/// Paint the box decoration in front of the children.
foreground,
}
/// Paints a [Decoration] either before or after its child paints.
class RenderDecoratedBox extends RenderProxyBox {
/// Creates a decorated box.
///
/// The [decoration], [position], and [configuration] arguments must not be
/// null. By default the decoration paints behind the child.
///
/// The [ImageConfiguration] will be passed to the decoration (with the size
/// filled in) to let it resolve images.
RenderDecoratedBox({
required Decoration decoration,
DecorationPosition position = DecorationPosition.background,
ImageConfiguration configuration = ImageConfiguration.empty,
RenderBox? child,
}) : assert(decoration != null),
assert(position != null),
assert(configuration != null),
_decoration = decoration,
_position = position,
_configuration = configuration,
super(child);
BoxPainter? _painter;
/// What decoration to paint.
///
/// Commonly a [BoxDecoration].
Decoration get decoration => _decoration;
Decoration _decoration;
set decoration(Decoration value) {
assert(value != null);
if (value == _decoration) {
return;
}
_painter?.dispose();
_painter = null;
_decoration = value;
markNeedsPaint();
}
/// Whether to paint the box decoration behind or in front of the child.
DecorationPosition get position => _position;
DecorationPosition _position;
set position(DecorationPosition value) {
assert(value != null);
if (value == _position) {
return;
}
_position = value;
markNeedsPaint();
}
/// The settings to pass to the decoration when painting, so that it can
/// resolve images appropriately. See [ImageProvider.resolve] and
/// [BoxPainter.paint].
///
/// The [ImageConfiguration.textDirection] field is also used by
/// direction-sensitive [Decoration]s for painting and hit-testing.
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration) {
return;
}
_configuration = value;
markNeedsPaint();
}
@override
void detach() {
_painter?.dispose();
_painter = null;
super.detach();
// Since we're disposing of our painter, we won't receive change
// notifications. We mark ourselves as needing paint so that we will
// resubscribe to change notifications. If we didn't do this, then, for
// example, animated GIFs would stop animating when a DecoratedBox gets
// moved around the tree due to GlobalKey reparenting.
markNeedsPaint();
}
@override
bool hitTestSelf(Offset position) {
return _decoration.hitTest(size, position, textDirection: configuration.textDirection);
}
@override
void paint(PaintingContext context, Offset offset) {
assert(size.width != null);
assert(size.height != null);
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {
int? debugSaveCount;
assert(() {
debugSaveCount = context.canvas.getSaveCount();
return true;
}());
_painter!.paint(context.canvas, offset, filledConfiguration);
assert(() {
if (debugSaveCount != context.canvas.getSaveCount()) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${_decoration.runtimeType} painter had mismatching save and restore calls.'),
ErrorDescription(
'Before painting the decoration, the canvas save count was $debugSaveCount. '
'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. '
'Every call to save() or saveLayer() must be matched by a call to restore().',
),
DiagnosticsProperty<Decoration>('The decoration was', decoration, style: DiagnosticsTreeStyle.errorProperty),
DiagnosticsProperty<BoxPainter>('The painter was', _painter, style: DiagnosticsTreeStyle.errorProperty),
]);
}
return true;
}());
if (decoration.isComplex) {
context.setIsComplexHint();
}
}
super.paint(context, offset);
if (position == DecorationPosition.foreground) {
_painter!.paint(context.canvas, offset, filledConfiguration);
if (decoration.isComplex) {
context.setIsComplexHint();
}
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(_decoration.toDiagnosticsNode(name: 'decoration'));
properties.add(DiagnosticsProperty<ImageConfiguration>('configuration', configuration));
}
}
/// Applies a transformation before painting its child.
class RenderTransform extends RenderProxyBox {
/// Creates a render object that transforms its child.
///
/// The [transform] argument must not be null.
RenderTransform({
required Matrix4 transform,
Offset? origin,
AlignmentGeometry? alignment,
TextDirection? textDirection,
this.transformHitTests = true,
FilterQuality? filterQuality,
RenderBox? child,
}) : assert(transform != null),
super(child) {
this.transform = transform;
this.alignment = alignment;
this.textDirection = textDirection;
this.filterQuality = filterQuality;
this.origin = origin;
}
/// The origin of the coordinate system (relative to the upper left corner of
/// this render object) in which to apply the matrix.
///
/// Setting an origin is equivalent to conjugating the transform matrix by a
/// translation. This property is provided just for convenience.
Offset? get origin => _origin;
Offset? _origin;
set origin(Offset? value) {
if (_origin == value) {
return;
}
_origin = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The alignment of the origin, relative to the size of the box.
///
/// This is equivalent to setting an origin based on the size of the box.
/// If it is specified at the same time as an offset, both are applied.
///
/// An [AlignmentDirectional.centerStart] value is the same as an [Alignment]
/// whose [Alignment.x] value is `-1.0` if [textDirection] is
/// [TextDirection.ltr], and `1.0` if [textDirection] is [TextDirection.rtl].
/// Similarly [AlignmentDirectional.centerEnd] is the same as an [Alignment]
/// whose [Alignment.x] value is `1.0` if [textDirection] is
/// [TextDirection.ltr], and `-1.0` if [textDirection] is [TextDirection.rtl].
AlignmentGeometry? get alignment => _alignment;
AlignmentGeometry? _alignment;
set alignment(AlignmentGeometry? value) {
if (_alignment == value) {
return;
}
_alignment = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The text direction with which to resolve [alignment].
///
/// This may be changed to null, but only after [alignment] has been changed
/// to a value that does not depend on the direction.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
@override
bool get alwaysNeedsCompositing => child != null && _filterQuality != null;
/// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation.
///
/// [applyPaintTransform], and therefore [localToGlobal] and [globalToLocal],
/// always honor the transformation, regardless of the value of this property.
bool transformHitTests;
Matrix4? _transform;
/// The matrix to transform the child by during painting. The provided value
/// is copied on assignment.
///
/// There is no getter for [transform], because [Matrix4] is mutable, and
/// mutations outside of the control of the render object could not reliably
/// be reflected in the rendering.
set transform(Matrix4 value) { // ignore: avoid_setters_without_getters
assert(value != null);
if (_transform == value) {
return;
}
_transform = Matrix4.copy(value);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The filter quality with which to apply the transform as a bitmap operation.
///
/// {@macro flutter.widgets.Transform.optional.FilterQuality}
FilterQuality? get filterQuality => _filterQuality;
FilterQuality? _filterQuality;
set filterQuality(FilterQuality? value) {
if (_filterQuality == value) {
return;
}
final bool didNeedCompositing = alwaysNeedsCompositing;
_filterQuality = value;
if (didNeedCompositing != alwaysNeedsCompositing) {
markNeedsCompositingBitsUpdate();
}
markNeedsPaint();
}
/// Sets the transform to the identity matrix.
void setIdentity() {
_transform!.setIdentity();
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a rotation about the x axis into the transform.
void rotateX(double radians) {
_transform!.rotateX(radians);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a rotation about the y axis into the transform.
void rotateY(double radians) {
_transform!.rotateY(radians);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a rotation about the z axis into the transform.
void rotateZ(double radians) {
_transform!.rotateZ(radians);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a translation by (x, y, z) into the transform.
void translate(double x, [ double y = 0.0, double z = 0.0 ]) {
_transform!.translate(x, y, z);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a scale into the transform.
void scale(double x, [ double? y, double? z ]) {
_transform!.scale(x, y, z);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
Matrix4? get _effectiveTransform {
final Alignment? resolvedAlignment = alignment?.resolve(textDirection);
if (_origin == null && resolvedAlignment == null) {
return _transform;
}
final Matrix4 result = Matrix4.identity();
if (_origin != null) {
result.translate(_origin!.dx, _origin!.dy);
}
Offset? translation;
if (resolvedAlignment != null) {
translation = resolvedAlignment.alongSize(size);
result.translate(translation.dx, translation.dy);
}
result.multiply(_transform!);
if (resolvedAlignment != null) {
result.translate(-translation!.dx, -translation.dy);
}
if (_origin != null) {
result.translate(-_origin!.dx, -_origin!.dy);
}
return result;
}
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// RenderTransform objects don't check if they are
// themselves hit, because it's confusing to think about
// how the untransformed size and the child's transformed
// position interact.
return hitTestChildren(result, position: position);
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
assert(!transformHitTests || _effectiveTransform != null);
return result.addWithPaintTransform(
transform: transformHitTests ? _effectiveTransform : null,
position: position,
hitTest: (BoxHitTestResult result, Offset position) {
return super.hitTestChildren(result, position: position);
},
);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Matrix4 transform = _effectiveTransform!;
if (filterQuality == null) {
final Offset? childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null) {
// if the matrix is singular the children would be compressed to a line or
// single point, instead short-circuit and paint nothing.
final double det = transform.determinant();
if (det == 0 || !det.isFinite) {
layer = null;
return;
}
layer = context.pushTransform(
needsCompositing,
offset,
transform,
super.paint,
oldLayer: layer is TransformLayer ? layer as TransformLayer? : null,
);
} else {
super.paint(context, offset + childOffset);
layer = null;
}
} else {
final Matrix4 effectiveTransform = Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)..translate(-offset.dx, -offset.dy);
final ui.ImageFilter filter = ui.ImageFilter.matrix(
effectiveTransform.storage,
filterQuality: filterQuality!,
);
if (layer is ImageFilterLayer) {
final ImageFilterLayer filterLayer = layer! as ImageFilterLayer;
filterLayer.imageFilter = filter;
} else {
layer = ImageFilterLayer(imageFilter: filter);
}
context.pushLayer(layer!, super.paint, offset);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
}
}
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.multiply(_effectiveTransform!);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(TransformProperty('transform matrix', _transform));
properties.add(DiagnosticsProperty<Offset>('origin', origin));
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('transformHitTests', transformHitTests));
}
}
/// Scales and positions its child within itself according to [fit].
class RenderFittedBox extends RenderProxyBox {
/// Scales and positions its child within itself.
///
/// The [fit] and [alignment] arguments must not be null.
RenderFittedBox({
BoxFit fit = BoxFit.contain,
AlignmentGeometry alignment = Alignment.center,
TextDirection? textDirection,
RenderBox? child,
Clip clipBehavior = Clip.none,
}) : assert(fit != null),
assert(alignment != null),
assert(clipBehavior != null),
_fit = fit,
_alignment = alignment,
_textDirection = textDirection,
_clipBehavior = clipBehavior,
super(child);
Alignment? _resolvedAlignment;
void _resolve() {
if (_resolvedAlignment != null) {
return;
}
_resolvedAlignment = alignment.resolve(textDirection);
}
void _markNeedResolution() {
_resolvedAlignment = null;
markNeedsPaint();
}
bool _fitAffectsLayout(BoxFit fit) {
switch (fit) {
case BoxFit.scaleDown:
return true;
case BoxFit.contain:
case BoxFit.cover:
case BoxFit.fill:
case BoxFit.fitHeight:
case BoxFit.fitWidth:
case BoxFit.none:
return false;
}
}
/// How to inscribe the child into the space allocated during layout.
BoxFit get fit => _fit;
BoxFit _fit;
set fit(BoxFit value) {
assert(value != null);
if (_fit == value) {
return;
}
final BoxFit lastFit = _fit;
_fit = value;
if (_fitAffectsLayout(lastFit) || _fitAffectsLayout(value)) {
markNeedsLayout();
} else {
_clearPaintData();
markNeedsPaint();
}
}
/// How to align the child within its parent's bounds.
///
/// An alignment of (0.0, 0.0) aligns the child to the top-left corner of its
/// parent's bounds. An alignment of (1.0, 0.5) aligns the child to the middle
/// of the right edge of its parent's bounds.
///
/// If this is set to an [AlignmentDirectional] object, then
/// [textDirection] must not be null.
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
set alignment(AlignmentGeometry value) {
assert(value != null);
if (_alignment == value) {
return;
}
_alignment = value;
_clearPaintData();
_markNeedResolution();
}
/// The text direction with which to resolve [alignment].
///
/// This may be changed to null, but only after [alignment] has been changed
/// to a value that does not depend on the direction.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
_clearPaintData();
_markNeedResolution();
}
// TODO(ianh): The intrinsic dimensions of this box are wrong.
@override
Size computeDryLayout(BoxConstraints constraints) {
if (child != null) {
final Size childSize = child!.getDryLayout(const BoxConstraints());
// During [RenderObject.debugCheckingIntrinsics] a child that doesn't
// support dry layout may provide us with an invalid size that triggers
// assertions if we try to work with it. Instead of throwing, we bail
// out early in that case.
bool invalidChildSize = false;
assert(() {
if (RenderObject.debugCheckingIntrinsics && childSize.width * childSize.height == 0.0) {
invalidChildSize = true;
}
return true;
}());
if (invalidChildSize) {
assert(debugCannotComputeDryLayout(
reason: 'Child provided invalid size of $childSize.',
));
return Size.zero;
}
switch (fit) {
case BoxFit.scaleDown:
final BoxConstraints sizeConstraints = constraints.loosen();
final Size unconstrainedSize = sizeConstraints.constrainSizeAndAttemptToPreserveAspectRatio(childSize);
return constraints.constrain(unconstrainedSize);
case BoxFit.contain:
case BoxFit.cover:
case BoxFit.fill:
case BoxFit.fitHeight:
case BoxFit.fitWidth:
case BoxFit.none:
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(childSize);
}
} else {
return constraints.smallest;
}
}
@override
void performLayout() {
if (child != null) {
child!.layout(const BoxConstraints(), parentUsesSize: true);
switch (fit) {
case BoxFit.scaleDown:
final BoxConstraints sizeConstraints = constraints.loosen();
final Size unconstrainedSize = sizeConstraints.constrainSizeAndAttemptToPreserveAspectRatio(child!.size);
size = constraints.constrain(unconstrainedSize);
break;
case BoxFit.contain:
case BoxFit.cover:
case BoxFit.fill:
case BoxFit.fitHeight:
case BoxFit.fitWidth:
case BoxFit.none:
size = constraints.constrainSizeAndAttemptToPreserveAspectRatio(child!.size);
break;
}
_clearPaintData();
} else {
size = constraints.smallest;
}
}
bool? _hasVisualOverflow;
Matrix4? _transform;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none], and must not be null.
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
void _clearPaintData() {
_hasVisualOverflow = null;
_transform = null;
}
void _updatePaintData() {
if (_transform != null) {
return;
}
if (child == null) {
_hasVisualOverflow = false;
_transform = Matrix4.identity();
} else {
_resolve();
final Size childSize = child!.size;
final FittedSizes sizes = applyBoxFit(_fit, childSize, size);
final double scaleX = sizes.destination.width / sizes.source.width;
final double scaleY = sizes.destination.height / sizes.source.height;
final Rect sourceRect = _resolvedAlignment!.inscribe(sizes.source, Offset.zero & childSize);
final Rect destinationRect = _resolvedAlignment!.inscribe(sizes.destination, Offset.zero & size);
_hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.height;
assert(scaleX.isFinite && scaleY.isFinite);
_transform = Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0)
..scale(scaleX, scaleY, 1.0)
..translate(-sourceRect.left, -sourceRect.top);
assert(_transform!.storage.every((double value) => value.isFinite));
}
}
TransformLayer? _paintChildWithTransform(PaintingContext context, Offset offset) {
final Offset? childOffset = MatrixUtils.getAsTranslation(_transform!);
if (childOffset == null) {
return context.pushTransform(
needsCompositing,
offset,
_transform!,
super.paint,
oldLayer: layer is TransformLayer ? layer! as TransformLayer : null,
);
} else {
super.paint(context, offset + childOffset);
}
return null;
}
@override
void paint(PaintingContext context, Offset offset) {
if (child == null || size.isEmpty || child!.size.isEmpty) {
return;
}
_updatePaintData();
assert(child != null);
if (_hasVisualOverflow! && clipBehavior != Clip.none) {
layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintChildWithTransform,
oldLayer: layer is ClipRectLayer ? layer! as ClipRectLayer : null,
clipBehavior: clipBehavior,
);
} else {
layer = _paintChildWithTransform(context, offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (size.isEmpty || (child?.size.isEmpty ?? false)) {
return false;
}
_updatePaintData();
return result.addWithPaintTransform(
transform: _transform,
position: position,
hitTest: (BoxHitTestResult result, Offset position) {
return super.hitTestChildren(result, position: position);
},
);
}
@override
bool paintsChild(RenderBox child) {
assert(child.parent == this);
return !size.isEmpty && !child.size.isEmpty;
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (!paintsChild(child)) {
transform.setZero();
} else {
_updatePaintData();
transform.multiply(_transform!);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<BoxFit>('fit', fit));
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
/// Applies a translation transformation before painting its child.
///
/// The translation is expressed as an [Offset] scaled to the child's size. For
/// example, an [Offset] with a `dx` of 0.25 will result in a horizontal
/// translation of one quarter the width of the child.
///
/// Hit tests will only be detected inside the bounds of the
/// [RenderFractionalTranslation], even if the contents are offset such that
/// they overflow.
class RenderFractionalTranslation extends RenderProxyBox {
/// Creates a render object that translates its child's painting.
///
/// The [translation] argument must not be null.
RenderFractionalTranslation({
required Offset translation,
this.transformHitTests = true,
RenderBox? child,
}) : assert(translation != null),
_translation = translation,
super(child);
/// The translation to apply to the child, scaled to the child's size.
///
/// For example, an [Offset] with a `dx` of 0.25 will result in a horizontal
/// translation of one quarter the width of the child.
Offset get translation => _translation;
Offset _translation;
set translation(Offset value) {
assert(value != null);
if (_translation == value) {
return;
}
_translation = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// RenderFractionalTranslation objects don't check if they are
// themselves hit, because it's confusing to think about
// how the untransformed size and the child's transformed
// position interact.
return hitTestChildren(result, position: position);
}
/// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation.
///
/// applyPaintTransform(), and therefore localToGlobal() and globalToLocal(),
/// always honor the transformation, regardless of the value of this property.
bool transformHitTests;
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
assert(!debugNeedsLayout);
return result.addWithPaintOffset(
offset: transformHitTests
? Offset(translation.dx * size.width, translation.dy * size.height)
: null,
position: position,
hitTest: (BoxHitTestResult result, Offset position) {
return super.hitTestChildren(result, position: position);
},
);
}
@override
void paint(PaintingContext context, Offset offset) {
assert(!debugNeedsLayout);
if (child != null) {
super.paint(context, Offset(
offset.dx + translation.dx * size.width,
offset.dy + translation.dy * size.height,
));
}
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(
translation.dx * size.width,
translation.dy * size.height,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Offset>('translation', translation));
properties.add(DiagnosticsProperty<bool>('transformHitTests', transformHitTests));
}
}
/// Signature for listening to [PointerDownEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerDownEventListener = void Function(PointerDownEvent event);
/// Signature for listening to [PointerMoveEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerMoveEventListener = void Function(PointerMoveEvent event);
/// Signature for listening to [PointerUpEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerUpEventListener = void Function(PointerUpEvent event);
/// Signature for listening to [PointerCancelEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerCancelEventListener = void Function(PointerCancelEvent event);
/// Signature for listening to [PointerPanZoomStartEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerPanZoomStartEventListener = void Function(PointerPanZoomStartEvent event);
/// Signature for listening to [PointerPanZoomUpdateEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerPanZoomUpdateEventListener = void Function(PointerPanZoomUpdateEvent event);
/// Signature for listening to [PointerPanZoomEndEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerPanZoomEndEventListener = void Function(PointerPanZoomEndEvent event);
/// Signature for listening to [PointerSignalEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef PointerSignalEventListener = void Function(PointerSignalEvent event);
/// Calls callbacks in response to common pointer events.
///
/// It responds to events that can construct gestures, such as when the
/// pointer is pointer is pressed and moved, and then released or canceled.
///
/// It does not respond to events that are exclusive to mouse, such as when the
/// mouse enters and exits a region without pressing any buttons. For
/// these events, use [RenderMouseRegion].
///
/// If it has a child, defers to the child for sizing behavior.
///
/// If it does not have a child, grows to fit the parent-provided constraints.
class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// Creates a render object that forwards pointer events to callbacks.
///
/// The [behavior] argument defaults to [HitTestBehavior.deferToChild].
RenderPointerListener({
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerHover,
this.onPointerCancel,
this.onPointerPanZoomStart,
this.onPointerPanZoomUpdate,
this.onPointerPanZoomEnd,
this.onPointerSignal,
super.behavior,
super.child,
});
/// Called when a pointer comes into contact with the screen (for touch
/// pointers), or has its button pressed (for mouse pointers) at this widget's
/// location.
PointerDownEventListener? onPointerDown;
/// Called when a pointer that triggered an [onPointerDown] changes position.
PointerMoveEventListener? onPointerMove;
/// Called when a pointer that triggered an [onPointerDown] is no longer in
/// contact with the screen.
PointerUpEventListener? onPointerUp;
/// Called when a pointer that has not an [onPointerDown] changes position.
PointerHoverEventListener? onPointerHover;
/// Called when the input from a pointer that triggered an [onPointerDown] is
/// no longer directed towards this receiver.
PointerCancelEventListener? onPointerCancel;
/// Called when a pan/zoom begins such as from a trackpad gesture.
PointerPanZoomStartEventListener? onPointerPanZoomStart;
/// Called when a pan/zoom is updated.
PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
/// Called when a pan/zoom finishes.
PointerPanZoomEndEventListener? onPointerPanZoomEnd;
/// Called when a pointer signal occurs over this object.
PointerSignalEventListener? onPointerSignal;
@override
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.biggest;
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent) {
return onPointerDown?.call(event);
}
if (event is PointerMoveEvent) {
return onPointerMove?.call(event);
}
if (event is PointerUpEvent) {
return onPointerUp?.call(event);
}
if (event is PointerHoverEvent) {
return onPointerHover?.call(event);
}
if (event is PointerCancelEvent) {
return onPointerCancel?.call(event);
}
if (event is PointerPanZoomStartEvent) {
return onPointerPanZoomStart?.call(event);
}
if (event is PointerPanZoomUpdateEvent) {
return onPointerPanZoomUpdate?.call(event);
}
if (event is PointerPanZoomEndEvent) {
return onPointerPanZoomEnd?.call(event);
}
if (event is PointerSignalEvent) {
return onPointerSignal?.call(event);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagsSummary<Function?>(
'listeners',
<String, Function?>{
'down': onPointerDown,
'move': onPointerMove,
'up': onPointerUp,
'hover': onPointerHover,
'cancel': onPointerCancel,
'panZoomStart': onPointerPanZoomStart,
'panZoomUpdate': onPointerPanZoomUpdate,
'panZoomEnd': onPointerPanZoomEnd,
'signal': onPointerSignal,
},
ifEmpty: '<none>',
));
}
}
/// Calls callbacks in response to pointer events that are exclusive to mice.
///
/// It responds to events that are related to hovering, i.e. when the mouse
/// enters, exits (with or without pressing buttons), or moves over a region
/// without pressing buttons.
///
/// It does not respond to common events that construct gestures, such as when
/// the pointer is pressed, moved, then released or canceled. For these events,
/// use [RenderPointerListener].
///
/// If it has a child, it defers to the child for sizing behavior.
///
/// If it does not have a child, it grows to fit the parent-provided constraints.
///
/// See also:
///
/// * [MouseRegion], a widget that listens to hover events using
/// [RenderMouseRegion].
class RenderMouseRegion extends RenderProxyBoxWithHitTestBehavior implements MouseTrackerAnnotation {
/// Creates a render object that forwards pointer events to callbacks.
///
/// All parameters are optional. By default this method creates an opaque
/// mouse region with no callbacks and cursor being [MouseCursor.defer]. The
/// [cursor] must not be null.
RenderMouseRegion({
this.onEnter,
this.onHover,
this.onExit,
MouseCursor cursor = MouseCursor.defer,
bool validForMouseTracker = true,
bool opaque = true,
super.child,
HitTestBehavior? hitTestBehavior = HitTestBehavior.opaque,
}) : assert(opaque != null),
assert(cursor != null),
_cursor = cursor,
_validForMouseTracker = validForMouseTracker,
_opaque = opaque,
super(behavior: hitTestBehavior ?? HitTestBehavior.opaque);
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return super.hitTest(result, position: position) && _opaque;
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (onHover != null && event is PointerHoverEvent) {
return onHover!(event);
}
}
/// Whether this object should prevent [RenderMouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
/// and [onExit] behave.
///
/// If [opaque] is true, this object will absorb the mouse pointer and
/// prevent this object's siblings (or any other objects that are not
/// ancestors or descendants of this object) from detecting the mouse
/// pointer even when the pointer is within their areas.
///
/// If [opaque] is false, this object will not affect how [RenderMouseRegion]s
/// behind it behave, which will detect the mouse pointer as long as the
/// pointer is within their areas.
///
/// This defaults to true.
bool get opaque => _opaque;
bool _opaque;
set opaque(bool value) {
if (_opaque != value) {
_opaque = value;
// Trigger [MouseTracker]'s device update to recalculate mouse states.
markNeedsPaint();
}
}
/// How to behave during hit testing.
///
/// This defaults to [HitTestBehavior.opaque] if null.
HitTestBehavior? get hitTestBehavior => behavior;
set hitTestBehavior(HitTestBehavior? value) {
final HitTestBehavior newValue = value ?? HitTestBehavior.opaque;
if (behavior != newValue) {
behavior = newValue;
// Trigger [MouseTracker]'s device update to recalculate mouse states.
markNeedsPaint();
}
}
@override
PointerEnterEventListener? onEnter;
/// Triggered when a pointer has moved onto or within the region without
/// buttons pressed.
///
/// This callback is not triggered by the movement of the object.
PointerHoverEventListener? onHover;
@override
PointerExitEventListener? onExit;
@override
MouseCursor get cursor => _cursor;
MouseCursor _cursor;
set cursor(MouseCursor value) {
if (_cursor != value) {
_cursor = value;
// A repaint is needed in order to trigger a device update of
// [MouseTracker] so that this new value can be found.
markNeedsPaint();
}
}
@override
bool get validForMouseTracker => _validForMouseTracker;
bool _validForMouseTracker;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_validForMouseTracker = true;
}
@override
void detach() {
// It's possible that the renderObject be detached during mouse events
// dispatching, set the [MouseTrackerAnnotation.validForMouseTracker] false to prevent
// the callbacks from being called.
_validForMouseTracker = false;
super.detach();
}
@override
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.biggest;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagsSummary<Function?>(
'listeners',
<String, Function?>{
'enter': onEnter,
'hover': onHover,
'exit': onExit,
},
ifEmpty: '<none>',
));
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: MouseCursor.defer));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
properties.add(FlagProperty('validForMouseTracker', value: validForMouseTracker, defaultValue: true, ifFalse: 'invalid for MouseTracker'));
}
}
/// Creates a separate display list for its child.
///
/// This render object creates a separate display list for its child, which
/// can improve performance if the subtree repaints at different times than
/// the surrounding parts of the tree. Specifically, when the child does not
/// repaint but its parent does, we can re-use the display list we recorded
/// previously. Similarly, when the child repaints but the surround tree does
/// not, we can re-record its display list without re-recording the display list
/// for the surround tree.
///
/// In some cases, it is necessary to place _two_ (or more) repaint boundaries
/// to get a useful effect. Consider, for example, an e-mail application that
/// shows an unread count and a list of e-mails. Whenever a new e-mail comes in,
/// the list would update, but so would the unread count. If only one of these
/// two parts of the application was behind a repaint boundary, the entire
/// application would repaint each time. On the other hand, if both were behind
/// a repaint boundary, a new e-mail would only change those two parts of the
/// application and the rest of the application would not repaint.
///
/// To tell if a particular RenderRepaintBoundary is useful, run your
/// application in debug mode, interacting with it in typical ways, and then
/// call [debugDumpRenderTree]. Each RenderRepaintBoundary will include the
/// ratio of cases where the repaint boundary was useful vs the cases where it
/// was not. These counts can also be inspected programmatically using
/// [debugAsymmetricPaintCount] and [debugSymmetricPaintCount] respectively.
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox? child }) : super(child);
@override
bool get isRepaintBoundary => true;
/// Capture an image of the current state of this render object and its
/// children.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes in the dimensions
/// of the render object, multiplied by the [pixelRatio].
///
/// To use [toImage], the render object must have gone through the paint phase
/// (i.e. [debugNeedsPaint] must be false).
///
/// The [pixelRatio] describes the scale between the logical pixels and the
/// size of the output image. It is independent of the
/// [dart:ui.FlutterView.devicePixelRatio] for the device, so specifying 1.0
/// (the default) will give you a 1:1 mapping between logical pixels and the
/// output pixels in the image.
///
/// {@tool snippet}
///
/// The following is an example of how to go from a `GlobalKey` on a
/// `RepaintBoundary` to a PNG:
///
/// ```dart
/// class PngHome extends StatefulWidget {
/// const PngHome({super.key});
///
/// @override
/// State<PngHome> createState() => _PngHomeState();
/// }
///
/// class _PngHomeState extends State<PngHome> {
/// GlobalKey globalKey = GlobalKey();
///
/// Future<void> _capturePng() async {
/// final RenderRepaintBoundary boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
/// final ui.Image image = await boundary.toImage();
/// final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
/// final Uint8List pngBytes = byteData!.buffer.asUint8List();
/// print(pngBytes);
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return RepaintBoundary(
/// key: globalKey,
/// child: Center(
/// child: TextButton(
/// onPressed: _capturePng,
/// child: const Text('Hello World', textDirection: TextDirection.ltr),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [OffsetLayer.toImage] for a similar API at the layer level.
/// * [dart:ui.Scene.toImage] for more information about the image returned.
Future<ui.Image> toImage({ double pixelRatio = 1.0 }) {
assert(!debugNeedsPaint);
final OffsetLayer offsetLayer = layer! as OffsetLayer;
return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
}
/// Capture an image of the current state of this render object and its
/// children synchronously.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes in the dimensions
/// of the render object, multiplied by the [pixelRatio].
///
/// To use [toImageSync], the render object must have gone through the paint phase
/// (i.e. [debugNeedsPaint] must be false).
///
/// The [pixelRatio] describes the scale between the logical pixels and the
/// size of the output image. It is independent of the
/// [dart:ui.FlutterView.devicePixelRatio] for the device, so specifying 1.0
/// (the default) will give you a 1:1 mapping between logical pixels and the
/// output pixels in the image.
///
/// This API functions like [toImage], except that rasterization begins eagerly
/// on the raster thread and the image is returned before this is completed.
///
/// {@tool snippet}
///
/// The following is an example of how to go from a `GlobalKey` on a
/// `RepaintBoundary` to an image handle:
///
/// ```dart
/// class ImageCaptureHome extends StatefulWidget {
/// const ImageCaptureHome({super.key});
///
/// @override
/// State<ImageCaptureHome> createState() => _ImageCaptureHomeState();
/// }
///
/// class _ImageCaptureHomeState extends State<ImageCaptureHome> {
/// GlobalKey globalKey = GlobalKey();
///
/// void _captureImage() {
/// final RenderRepaintBoundary boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
/// final ui.Image image = boundary.toImageSync();
/// print('Image dimensions: ${image.width}x${image.height}');
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return RepaintBoundary(
/// key: globalKey,
/// child: Center(
/// child: TextButton(
/// onPressed: _captureImage,
/// child: const Text('Hello World', textDirection: TextDirection.ltr),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [OffsetLayer.toImageSync] for a similar API at the layer level.
/// * [dart:ui.Scene.toImageSync] for more information about the image returned.
ui.Image toImageSync({ double pixelRatio = 1.0 }) {
assert(!debugNeedsPaint);
final OffsetLayer offsetLayer = layer! as OffsetLayer;
return offsetLayer.toImageSync(Offset.zero & size, pixelRatio: pixelRatio);
}
/// The number of times that this render object repainted at the same time as
/// its parent. Repaint boundaries are only useful when the parent and child
/// paint at different times. When both paint at the same time, the repaint
/// boundary is redundant, and may be actually making performance worse.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// zero.
///
/// Can be reset using [debugResetMetrics]. See [debugAsymmetricPaintCount]
/// for the corresponding count of times where only the parent or only the
/// child painted.
int get debugSymmetricPaintCount => _debugSymmetricPaintCount;
int _debugSymmetricPaintCount = 0;
/// The number of times that either this render object repainted without the
/// parent being painted, or the parent repainted without this object being
/// painted. When a repaint boundary is used at a seam in the render tree
/// where the parent tends to repaint at entirely different times than the
/// child, it can improve performance by reducing the number of paint
/// operations that have to be recorded each frame.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// zero.
///
/// Can be reset using [debugResetMetrics]. See [debugSymmetricPaintCount] for
/// the corresponding count of times where both the parent and the child
/// painted together.
int get debugAsymmetricPaintCount => _debugAsymmetricPaintCount;
int _debugAsymmetricPaintCount = 0;
/// Resets the [debugSymmetricPaintCount] and [debugAsymmetricPaintCount]
/// counts to zero.
///
/// Only valid when asserts are enabled. Does nothing in release builds.
void debugResetMetrics() {
assert(() {
_debugSymmetricPaintCount = 0;
_debugAsymmetricPaintCount = 0;
return true;
}());
}
@override
void debugRegisterRepaintBoundaryPaint({ bool includedParent = true, bool includedChild = false }) {
assert(() {
if (includedParent && includedChild) {
_debugSymmetricPaintCount += 1;
} else {
_debugAsymmetricPaintCount += 1;
}
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
bool inReleaseMode = true;
assert(() {
inReleaseMode = false;
if (debugSymmetricPaintCount + debugAsymmetricPaintCount == 0) {
properties.add(MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)'));
} else {
final double fraction = debugAsymmetricPaintCount / (debugSymmetricPaintCount + debugAsymmetricPaintCount);
final String diagnosis;
if (debugSymmetricPaintCount + debugAsymmetricPaintCount < 5) {
diagnosis = 'insufficient data to draw conclusion (less than five repaints)';
} else if (fraction > 0.9) {
diagnosis = 'this is an outstandingly useful repaint boundary and should definitely be kept';
} else if (fraction > 0.5) {
diagnosis = 'this is a useful repaint boundary and should be kept';
} else if (fraction > 0.30) {
diagnosis = 'this repaint boundary is probably useful, but maybe it would be more useful in tandem with adding more repaint boundaries elsewhere';
} else if (fraction > 0.1) {
diagnosis = 'this repaint boundary does sometimes show value, though currently not that often';
} else if (debugAsymmetricPaintCount == 0) {
diagnosis = 'this repaint boundary is astoundingly ineffectual and should be removed';
} else {
diagnosis = 'this repaint boundary is not very effective and should probably be removed';
}
properties.add(PercentProperty('metrics', fraction, unit: 'useful', tooltip: '$debugSymmetricPaintCount bad vs $debugAsymmetricPaintCount good'));
properties.add(MessageProperty('diagnosis', diagnosis));
}
return true;
}());
if (inReleaseMode) {
properties.add(DiagnosticsNode.message('(run in debug mode to collect repaint boundary statistics)'));
}
}
}
/// A render object that is invisible during hit testing.
///
/// When [ignoring] is true, this render object (and its subtree) is invisible
/// to hit testing. It still consumes space during layout and paints its child
/// as usual. It just cannot be the target of located events, because its render
/// object returns false from [hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools). If
/// [ignoringSemantics] is null, it uses the value of [ignoring].
///
/// See also:
///
/// * [RenderAbsorbPointer], which takes the pointer events but prevents any
/// nodes in the subtree from seeing them.
class RenderIgnorePointer extends RenderProxyBox {
/// Creates a render object that is invisible to hit testing.
///
/// The [ignoring] argument must not be null. If [ignoringSemantics] is null,
/// this render object will be ignored for semantics if [ignoring] is true.
RenderIgnorePointer({
RenderBox? child,
bool ignoring = true,
bool? ignoringSemantics,
}) : _ignoring = ignoring,
_ignoringSemantics = ignoringSemantics,
super(child) {
assert(_ignoring != null);
}
/// Whether this render object is ignored during hit testing.
///
/// Regardless of whether this render object is ignored during hit testing, it
/// will still consume space during layout and be visible during painting.
bool get ignoring => _ignoring;
bool _ignoring;
set ignoring(bool value) {
assert(value != null);
if (value == _ignoring) {
return;
}
_ignoring = value;
if (_ignoringSemantics == null || !_ignoringSemantics!) {
markNeedsSemanticsUpdate();
}
}
/// Whether the semantics of this render object is ignored when compiling the semantics tree.
///
/// If null, defaults to value of [ignoring].
///
/// See [SemanticsNode] for additional information about the semantics tree.
bool? get ignoringSemantics => _ignoringSemantics;
bool? _ignoringSemantics;
set ignoringSemantics(bool? value) {
if (value == _ignoringSemantics) {
return;
}
final bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics) {
markNeedsSemanticsUpdate();
}
}
bool get _effectiveIgnoringSemantics => ignoringSemantics ?? ignoring;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return !ignoring && super.hitTest(result, position: position);
}
// TODO(ianh): figure out a way to still include labels and flags in
// descendants, just make them non-interactive, even when
// _effectiveIgnoringSemantics is true
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics) {
visitor(child!);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('ignoring', ignoring));
properties.add(
DiagnosticsProperty<bool>(
'ignoringSemantics',
_effectiveIgnoringSemantics,
description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,
),
);
}
}
/// Lays the child out as if it was in the tree, but without painting anything,
/// without making the child available for hit testing, and without taking any
/// room in the parent.
class RenderOffstage extends RenderProxyBox {
/// Creates an offstage render object.
RenderOffstage({
bool offstage = true,
RenderBox? child,
}) : assert(offstage != null),
_offstage = offstage,
super(child);
/// Whether the child is hidden from the rest of the tree.
///
/// If true, the child is laid out as if it was in the tree, but without
/// painting anything, without making the child available for hit testing, and
/// without taking any room in the parent.
///
/// If false, the child is included in the tree as normal.
bool get offstage => _offstage;
bool _offstage;
set offstage(bool value) {
assert(value != null);
if (value == _offstage) {
return;
}
_offstage = value;
markNeedsLayoutForSizedByParentChange();
}
@override
double computeMinIntrinsicWidth(double height) {
if (offstage) {
return 0.0;
}
return super.computeMinIntrinsicWidth(height);
}
@override
double computeMaxIntrinsicWidth(double height) {
if (offstage) {
return 0.0;
}
return super.computeMaxIntrinsicWidth(height);
}
@override
double computeMinIntrinsicHeight(double width) {
if (offstage) {
return 0.0;
}
return super.computeMinIntrinsicHeight(width);
}
@override
double computeMaxIntrinsicHeight(double width) {
if (offstage) {
return 0.0;
}
return super.computeMaxIntrinsicHeight(width);
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
if (offstage) {
return null;
}
return super.computeDistanceToActualBaseline(baseline);
}
@override
bool get sizedByParent => offstage;
@override
Size computeDryLayout(BoxConstraints constraints) {
if (offstage) {
return constraints.smallest;
}
return super.computeDryLayout(constraints);
}
@override
void performResize() {
assert(offstage);
super.performResize();
}
@override
void performLayout() {
if (offstage) {
child?.layout(constraints);
} else {
super.performLayout();
}
}
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return !offstage && super.hitTest(result, position: position);
}
@override
bool paintsChild(RenderBox child) {
assert(child.parent == this);
return !offstage;
}
@override
void paint(PaintingContext context, Offset offset) {
if (offstage) {
return;
}
super.paint(context, offset);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (offstage) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('offstage', offstage));
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
if (child == null) {
return <DiagnosticsNode>[];
}
return <DiagnosticsNode>[
child!.toDiagnosticsNode(
name: 'child',
style: offstage ? DiagnosticsTreeStyle.offstage : DiagnosticsTreeStyle.sparse,
),
];
}
}
/// A render object that absorbs pointers during hit testing.
///
/// When [absorbing] is true, this render object prevents its subtree from
/// receiving pointer events by terminating hit testing at itself. It still
/// consumes space during layout and paints its child as usual. It just prevents
/// its children from being the target of located events, because its render
/// object returns true from [hitTest].
///
/// See also:
///
/// * [RenderIgnorePointer], which has the opposite effect: removing the
/// subtree from considering entirely for the purposes of hit testing.
class RenderAbsorbPointer extends RenderProxyBox {
/// Creates a render object that absorbs pointers during hit testing.
///
/// The [absorbing] argument must not be null.
RenderAbsorbPointer({
RenderBox? child,
bool absorbing = true,
bool? ignoringSemantics,
}) : assert(absorbing != null),
_absorbing = absorbing,
_ignoringSemantics = ignoringSemantics,
super(child);
/// Whether this render object absorbs pointers during hit testing.
///
/// Regardless of whether this render object absorbs pointers during hit
/// testing, it will still consume space during layout and be visible during
/// painting.
bool get absorbing => _absorbing;
bool _absorbing;
set absorbing(bool value) {
if (_absorbing == value) {
return;
}
_absorbing = value;
if (ignoringSemantics == null) {
markNeedsSemanticsUpdate();
}
}
/// Whether the semantics of this render object is ignored when compiling the semantics tree.
///
/// If null, defaults to value of [absorbing].
///
/// See [SemanticsNode] for additional information about the semantics tree.
bool? get ignoringSemantics => _ignoringSemantics;
bool? _ignoringSemantics;
set ignoringSemantics(bool? value) {
if (value == _ignoringSemantics) {
return;
}
final bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics) {
markNeedsSemanticsUpdate();
}
}
bool get _effectiveIgnoringSemantics => ignoringSemantics ?? absorbing;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return absorbing
? size.contains(position)
: super.hitTest(result, position: position);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics) {
visitor(child!);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('absorbing', absorbing));
properties.add(
DiagnosticsProperty<bool>(
'ignoringSemantics',
_effectiveIgnoringSemantics,
description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,
),
);
}
}
/// Holds opaque meta data in the render tree.
///
/// Useful for decorating the render tree with information that will be consumed
/// later. For example, you could store information in the render tree that will
/// be used when the user interacts with the render tree but has no visual
/// impact prior to the interaction.
class RenderMetaData extends RenderProxyBoxWithHitTestBehavior {
/// Creates a render object that hold opaque meta data.
///
/// The [behavior] argument defaults to [HitTestBehavior.deferToChild].
RenderMetaData({
this.metaData,
super.behavior,
super.child,
});
/// Opaque meta data ignored by the render tree.
dynamic metaData;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<dynamic>('metaData', metaData));
}
}
/// Listens for the specified gestures from the semantics server (e.g.
/// an accessibility tool).
class RenderSemanticsGestureHandler extends RenderProxyBoxWithHitTestBehavior {
/// Creates a render object that listens for specific semantic gestures.
///
/// The [scrollFactor] and [behavior] arguments must not be null.
RenderSemanticsGestureHandler({
super.child,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
GestureDragUpdateCallback? onHorizontalDragUpdate,
GestureDragUpdateCallback? onVerticalDragUpdate,
this.scrollFactor = 0.8,
super.behavior,
}) : assert(scrollFactor != null),
_onTap = onTap,
_onLongPress = onLongPress,
_onHorizontalDragUpdate = onHorizontalDragUpdate,
_onVerticalDragUpdate = onVerticalDragUpdate;
/// If non-null, the set of actions to allow. Other actions will be omitted,
/// even if their callback is provided.
///
/// For example, if [onTap] is non-null but [validActions] does not contain
/// [SemanticsAction.tap], then the semantic description of this node will
/// not claim to support taps.
///
/// This is normally used to filter the actions made available by
/// [onHorizontalDragUpdate] and [onVerticalDragUpdate]. Normally, these make
/// both the right and left, or up and down, actions available. For example,
/// if [onHorizontalDragUpdate] is set but [validActions] only contains
/// [SemanticsAction.scrollLeft], then the [SemanticsAction.scrollRight]
/// action will be omitted.
Set<SemanticsAction>? get validActions => _validActions;
Set<SemanticsAction>? _validActions;
set validActions(Set<SemanticsAction>? value) {
if (setEquals<SemanticsAction>(value, _validActions)) {
return;
}
_validActions = value;
markNeedsSemanticsUpdate();
}
/// Called when the user taps on the render object.
GestureTapCallback? get onTap => _onTap;
GestureTapCallback? _onTap;
set onTap(GestureTapCallback? value) {
if (_onTap == value) {
return;
}
final bool hadHandler = _onTap != null;
_onTap = value;
if ((value != null) != hadHandler) {
markNeedsSemanticsUpdate();
}
}
/// Called when the user presses on the render object for a long period of time.
GestureLongPressCallback? get onLongPress => _onLongPress;
GestureLongPressCallback? _onLongPress;
set onLongPress(GestureLongPressCallback? value) {
if (_onLongPress == value) {
return;
}
final bool hadHandler = _onLongPress != null;
_onLongPress = value;
if ((value != null) != hadHandler) {
markNeedsSemanticsUpdate();
}
}
/// Called when the user scrolls to the left or to the right.
GestureDragUpdateCallback? get onHorizontalDragUpdate => _onHorizontalDragUpdate;
GestureDragUpdateCallback? _onHorizontalDragUpdate;
set onHorizontalDragUpdate(GestureDragUpdateCallback? value) {
if (_onHorizontalDragUpdate == value) {
return;
}
final bool hadHandler = _onHorizontalDragUpdate != null;
_onHorizontalDragUpdate = value;
if ((value != null) != hadHandler) {
markNeedsSemanticsUpdate();
}
}
/// Called when the user scrolls up or down.
GestureDragUpdateCallback? get onVerticalDragUpdate => _onVerticalDragUpdate;
GestureDragUpdateCallback? _onVerticalDragUpdate;
set onVerticalDragUpdate(GestureDragUpdateCallback? value) {
if (_onVerticalDragUpdate == value) {
return;
}
final bool hadHandler = _onVerticalDragUpdate != null;
_onVerticalDragUpdate = value;
if ((value != null) != hadHandler) {
markNeedsSemanticsUpdate();
}
}
/// The fraction of the dimension of this render box to use when
/// scrolling. For example, if this is 0.8 and the box is 200 pixels
/// wide, then when a left-scroll action is received from the
/// accessibility system, it will translate into a 160 pixel
/// leftwards drag.
double scrollFactor;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
if (onTap != null && _isValidAction(SemanticsAction.tap)) {
config.onTap = onTap;
}
if (onLongPress != null && _isValidAction(SemanticsAction.longPress)) {
config.onLongPress = onLongPress;
}
if (onHorizontalDragUpdate != null) {
if (_isValidAction(SemanticsAction.scrollRight)) {
config.onScrollRight = _performSemanticScrollRight;
}
if (_isValidAction(SemanticsAction.scrollLeft)) {
config.onScrollLeft = _performSemanticScrollLeft;
}
}
if (onVerticalDragUpdate != null) {
if (_isValidAction(SemanticsAction.scrollUp)) {
config.onScrollUp = _performSemanticScrollUp;
}
if (_isValidAction(SemanticsAction.scrollDown)) {
config.onScrollDown = _performSemanticScrollDown;
}
}
}
bool _isValidAction(SemanticsAction action) {
return validActions == null || validActions!.contains(action);
}
void _performSemanticScrollLeft() {
if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * -scrollFactor;
onHorizontalDragUpdate!(DragUpdateDetails(
delta: Offset(primaryDelta, 0.0), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
void _performSemanticScrollRight() {
if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * scrollFactor;
onHorizontalDragUpdate!(DragUpdateDetails(
delta: Offset(primaryDelta, 0.0), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
void _performSemanticScrollUp() {
if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * -scrollFactor;
onVerticalDragUpdate!(DragUpdateDetails(
delta: Offset(0.0, primaryDelta), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
void _performSemanticScrollDown() {
if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * scrollFactor;
onVerticalDragUpdate!(DragUpdateDetails(
delta: Offset(0.0, primaryDelta), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final List<String> gestures = <String>[
if (onTap != null) 'tap',
if (onLongPress != null) 'long press',
if (onHorizontalDragUpdate != null) 'horizontal scroll',
if (onVerticalDragUpdate != null) 'vertical scroll',
];
if (gestures.isEmpty) {
gestures.add('<none>');
}
properties.add(IterableProperty<String>('gestures', gestures));
}
}
/// Add annotations to the [SemanticsNode] for this subtree.
class RenderSemanticsAnnotations extends RenderProxyBox {
/// Creates a render object that attaches a semantic annotation.
///
/// The [container] argument must not be null.
///
/// If the [SemanticsProperties.attributedLabel] is not null, the [textDirection] must also not be null.
RenderSemanticsAnnotations({
RenderBox? child,
required SemanticsProperties properties,
bool container = false,
bool explicitChildNodes = false,
bool excludeSemantics = false,
TextDirection? textDirection,
}) : assert(container != null),
_container = container,
_explicitChildNodes = explicitChildNodes,
_excludeSemantics = excludeSemantics,
_textDirection = textDirection,
_properties = properties,
super(child) {
_updateAttributedFields(_properties);
}
/// All of the [SemanticsProperties] for this [RenderSemanticsAnnotations].
SemanticsProperties get properties => _properties;
SemanticsProperties _properties;
set properties(SemanticsProperties value) {
assert(value != null);
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;
bool _container;
set container(bool value) {
assert(value != null);
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;
bool _explicitChildNodes;
set explicitChildNodes(bool value) {
assert(value != null);
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;
bool _excludeSemantics;
set excludeSemantics(bool value) {
assert(value != null);
if (_excludeSemantics == value) {
return;
}
_excludeSemantics = value;
markNeedsSemanticsUpdate();
}
void _updateAttributedFields(SemanticsProperties value) {
_attributedLabel = _effectiveAttributedLabel(value);
_attributedValue = _effectiveAttributedValue(value);
_attributedIncreasedValue = _effectiveAttributedIncreasedValue(value);
_attributedDecreasedValue = _effectiveAttributedDecreasedValue(value);
_attributedHint = _effectiveAttributedHint(value);
}
AttributedString? _effectiveAttributedLabel(SemanticsProperties value) {
return value.attributedLabel ??
(value.label == null ? null : AttributedString(value.label!));
}
AttributedString? _effectiveAttributedValue(SemanticsProperties value) {
return value.attributedValue ??
(value.value == null ? null : AttributedString(value.value!));
}
AttributedString? _effectiveAttributedIncreasedValue(
SemanticsProperties value) {
return value.attributedIncreasedValue ??
(value.increasedValue == null
? null
: AttributedString(value.increasedValue!));
}
AttributedString? _effectiveAttributedDecreasedValue(
SemanticsProperties value) {
return properties.attributedDecreasedValue ??
(value.decreasedValue == null
? null
: AttributedString(value.decreasedValue!));
}
AttributedString? _effectiveAttributedHint(SemanticsProperties value) {
return value.attributedHint ??
(value.hint == null ? null : AttributedString(value.hint!));
}
AttributedString? _attributedLabel;
AttributedString? _attributedValue;
AttributedString? _attributedIncreasedValue;
AttributedString? _attributedDecreasedValue;
AttributedString? _attributedHint;
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given
/// value.
///
/// This must not be null if [SemanticsProperties.attributedLabel],
/// [SemanticsProperties.attributedHint],
/// [SemanticsProperties.attributedValue],
/// [SemanticsProperties.attributedIncreasedValue], or
/// [SemanticsProperties.attributedDecreasedValue] are not null.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (textDirection == value) {
return;
}
_textDirection = value;
markNeedsSemanticsUpdate();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (excludeSemantics) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = container;
config.explicitChildNodes = explicitChildNodes;
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.link != null) {
config.isLink = _properties.link!;
}
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.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.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 (_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!);
}
// 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.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();
}
}
/// Causes the semantics of all earlier render objects below the same semantic
/// boundary to be dropped.
///
/// This is useful in a stack where an opaque mask should prevent interactions
/// with the render objects painted below the mask.
class RenderBlockSemantics extends RenderProxyBox {
/// Create a render object that blocks semantics for nodes below it in paint
/// order.
RenderBlockSemantics({
RenderBox? child,
bool blocking = true,
}) : _blocking = blocking,
super(child);
/// Whether this render object is blocking semantics of previously painted
/// [RenderObject]s below a common semantics boundary from the semantic tree.
bool get blocking => _blocking;
bool _blocking;
set blocking(bool value) {
assert(value != null);
if (value == _blocking) {
return;
}
_blocking = value;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isBlockingSemanticsOfPreviouslyPaintedNodes = blocking;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('blocking', blocking));
}
}
/// Causes the semantics of all descendants to be merged into this
/// node such that the entire subtree becomes a single leaf in the
/// semantics tree.
///
/// Useful for combining the semantics of multiple render objects that
/// form part of a single conceptual widget, e.g. a checkbox, a label,
/// and the gesture detector that goes with them.
class RenderMergeSemantics extends RenderProxyBox {
/// Creates a render object that merges the semantics from its descendants.
RenderMergeSemantics({ RenderBox? child }) : super(child);
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
}
}
/// Excludes this subtree from the semantic tree.
///
/// When [excluding] is true, this render object (and its subtree) is excluded
/// from the semantic tree.
///
/// Useful e.g. for hiding text that is redundant with other text next
/// to it (e.g. text included only for the visual effect).
class RenderExcludeSemantics extends RenderProxyBox {
/// Creates a render object that ignores the semantics of its subtree.
RenderExcludeSemantics({
RenderBox? child,
bool excluding = true,
}) : _excluding = excluding,
super(child) {
assert(_excluding != null);
}
/// Whether this render object is excluded from the semantic tree.
bool get excluding => _excluding;
bool _excluding;
set excluding(bool value) {
assert(value != null);
if (value == _excluding) {
return;
}
_excluding = value;
markNeedsSemanticsUpdate();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (excluding) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('excluding', excluding));
}
}
/// A render objects that annotates semantics with an index.
///
/// Certain widgets will automatically provide a child index for building
/// semantics. For example, the [ScrollView] uses the index of the first
/// visible child semantics node to determine the
/// [SemanticsConfiguration.scrollIndex].
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
class RenderIndexedSemantics extends RenderProxyBox {
/// Creates a render object that annotates the child semantics with an index.
RenderIndexedSemantics({
RenderBox? child,
required int index,
}) : assert(index != null),
_index = index,
super(child);
/// The index used to annotated child semantics.
int get index => _index;
int _index;
set index(int value) {
if (value == index) {
return;
}
_index = value;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.indexInParent = index;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<int>('index', index));
}
}
/// Provides an anchor for a [RenderFollowerLayer].
///
/// See also:
///
/// * [CompositedTransformTarget], the corresponding widget.
/// * [LeaderLayer], the layer that this render object creates.
class RenderLeaderLayer extends RenderProxyBox {
/// Creates a render object that uses a [LeaderLayer].
///
/// The [link] must not be null.
RenderLeaderLayer({
required LayerLink link,
RenderBox? child,
}) : assert(link != null),
_link = link,
super(child);
/// The link object that connects this [RenderLeaderLayer] with one or more
/// [RenderFollowerLayer]s.
///
/// This property must not be null. The object must not be associated with
/// another [RenderLeaderLayer] that is also being painted.
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
assert(value != null);
if (_link == value) {
return;
}
_link.leaderSize = null;
_link = value;
if (_previousLayoutSize != null) {
_link.leaderSize = _previousLayoutSize;
}
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => true;
// The latest size of this [RenderBox], computed during the previous layout
// pass. It should always be equal to [size], but can be accessed even when
// [debugDoingThisResize] and [debugDoingThisLayout] are false.
Size? _previousLayoutSize;
@override
void performLayout() {
super.performLayout();
_previousLayoutSize = size;
link.leaderSize = size;
}
@override
void paint(PaintingContext context, Offset offset) {
if (layer == null) {
layer = LeaderLayer(link: link, offset: offset);
} else {
final LeaderLayer leaderLayer = layer! as LeaderLayer;
leaderLayer
..link = link
..offset = offset;
}
context.pushLayer(layer!, super.paint, Offset.zero);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LayerLink>('link', link));
}
}
/// Transform the child so that its origin is [offset] from the origin of the
/// [RenderLeaderLayer] with the same [LayerLink].
///
/// The [RenderLeaderLayer] in question must be earlier in the paint order.
///
/// Hit testing on descendants of this render object will only work if the
/// target position is within the box that this render object's parent considers
/// to be hittable.
///
/// See also:
///
/// * [CompositedTransformFollower], the corresponding widget.
/// * [FollowerLayer], the layer that this render object creates.
class RenderFollowerLayer extends RenderProxyBox {
/// Creates a render object that uses a [FollowerLayer].
///
/// The [link] and [offset] arguments must not be null.
RenderFollowerLayer({
required LayerLink link,
bool showWhenUnlinked = true,
Offset offset = Offset.zero,
Alignment leaderAnchor = Alignment.topLeft,
Alignment followerAnchor = Alignment.topLeft,
RenderBox? child,
}) : assert(link != null),
assert(showWhenUnlinked != null),
assert(offset != null),
_link = link,
_showWhenUnlinked = showWhenUnlinked,
_offset = offset,
_leaderAnchor = leaderAnchor,
_followerAnchor = followerAnchor,
super(child);
/// The link object that connects this [RenderFollowerLayer] with a
/// [RenderLeaderLayer] earlier in the paint order.
LayerLink get link => _link;
LayerLink _link;
set link(LayerLink value) {
assert(value != null);
if (_link == value) {
return;
}
_link = value;
markNeedsPaint();
}
/// Whether to show the render object's contents when there is no
/// corresponding [RenderLeaderLayer] with the same [link].
///
/// When the render object is linked, the child is positioned such that it has
/// the same global position as the linked [RenderLeaderLayer].
///
/// When the render object is not linked, then: if [showWhenUnlinked] is true,
/// the child is visible and not repositioned; if it is false, then child is
/// hidden, and its hit testing is also disabled.
bool get showWhenUnlinked => _showWhenUnlinked;
bool _showWhenUnlinked;
set showWhenUnlinked(bool value) {
assert(value != null);
if (_showWhenUnlinked == value) {
return;
}
_showWhenUnlinked = value;
markNeedsPaint();
}
/// The offset to apply to the origin of the linked [RenderLeaderLayer] to
/// obtain this render object's origin.
Offset get offset => _offset;
Offset _offset;
set offset(Offset value) {
assert(value != null);
if (_offset == value) {
return;
}
_offset = value;
markNeedsPaint();
}
/// The anchor point on the linked [RenderLeaderLayer] that [followerAnchor]
/// will line up with.
///
/// {@template flutter.rendering.RenderFollowerLayer.leaderAnchor}
/// For example, when [leaderAnchor] and [followerAnchor] are both
/// [Alignment.topLeft], this [RenderFollowerLayer] will be top left aligned
/// with the linked [RenderLeaderLayer]. When [leaderAnchor] is
/// [Alignment.bottomLeft] and [followerAnchor] is [Alignment.topLeft], this
/// [RenderFollowerLayer] will be left aligned with the linked
/// [RenderLeaderLayer], and its top edge will line up with the
/// [RenderLeaderLayer]'s bottom edge.
/// {@endtemplate}
///
/// Defaults to [Alignment.topLeft].
Alignment get leaderAnchor => _leaderAnchor;
Alignment _leaderAnchor;
set leaderAnchor(Alignment value) {
assert(value != null);
if (_leaderAnchor == value) {
return;
}
_leaderAnchor = value;
markNeedsPaint();
}
/// The anchor point on this [RenderFollowerLayer] that will line up with
/// [followerAnchor] on the linked [RenderLeaderLayer].
///
/// {@macro flutter.rendering.RenderFollowerLayer.leaderAnchor}
///
/// Defaults to [Alignment.topLeft].
Alignment get followerAnchor => _followerAnchor;
Alignment _followerAnchor;
set followerAnchor(Alignment value) {
assert(value != null);
if (_followerAnchor == value) {
return;
}
_followerAnchor = value;
markNeedsPaint();
}
@override
void detach() {
layer = null;
super.detach();
}
@override
bool get alwaysNeedsCompositing => true;
/// The layer we created when we were last painted.
@override
FollowerLayer? get layer => super.layer as FollowerLayer?;
/// Return the transform that was used in the last composition phase, if any.
///
/// If the [FollowerLayer] has not yet been created, was never composited, or
/// was unable to determine the transform (see
/// [FollowerLayer.getLastTransform]), this returns the identity matrix (see
/// [Matrix4.identity].
Matrix4 getCurrentTransform() {
return layer?.getLastTransform() ?? Matrix4.identity();
}
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// Disables the hit testing if this render object is hidden.
if (link.leader == null && !showWhenUnlinked) {
return false;
}
// RenderFollowerLayer objects don't check if they are
// themselves hit, because it's confusing to think about
// how the untransformed size and the child's transformed
// position interact.
return hitTestChildren(result, position: position);
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return result.addWithPaintTransform(
transform: getCurrentTransform(),
position: position,
hitTest: (BoxHitTestResult result, Offset position) {
return super.hitTestChildren(result, position: position);
},
);
}
@override
void paint(PaintingContext context, Offset offset) {
final Size? leaderSize = link.leaderSize;
assert(
link.leaderSize != null || link.leader == null || leaderAnchor == Alignment.topLeft,
'$link: layer is linked to ${link.leader} but a valid leaderSize is not set. '
'leaderSize is required when leaderAnchor is not Alignment.topLeft '
'(current value is $leaderAnchor).',
);
final Offset effectiveLinkedOffset = leaderSize == null
? this.offset
: leaderAnchor.alongSize(leaderSize) - followerAnchor.alongSize(size) + this.offset;
assert(showWhenUnlinked != null);
if (layer == null) {
layer = FollowerLayer(
link: link,
showWhenUnlinked: showWhenUnlinked,
linkedOffset: effectiveLinkedOffset,
unlinkedOffset: offset,
);
} else {
layer
?..link = link
..showWhenUnlinked = showWhenUnlinked
..linkedOffset = effectiveLinkedOffset
..unlinkedOffset = offset;
}
context.pushLayer(
layer!,
super.paint,
Offset.zero,
childPaintBounds: const Rect.fromLTRB(
// We don't know where we'll end up, so we have no idea what our cull rect should be.
double.negativeInfinity,
double.negativeInfinity,
double.infinity,
double.infinity,
),
);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.multiply(getCurrentTransform());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LayerLink>('link', link));
properties.add(DiagnosticsProperty<bool>('showWhenUnlinked', showWhenUnlinked));
properties.add(DiagnosticsProperty<Offset>('offset', offset));
properties.add(TransformProperty('current transform matrix', getCurrentTransform()));
}
}
/// Render object which inserts an [AnnotatedRegionLayer] into the layer tree.
///
/// See also:
///
/// * [Layer.find], for an example of how this value is retrieved.
/// * [AnnotatedRegionLayer], the layer this render object creates.
class RenderAnnotatedRegion<T extends Object> extends RenderProxyBox {
/// Creates a new [RenderAnnotatedRegion] to insert [value] into the
/// layer tree.
///
/// If [sized] is true, the layer is provided with the size of this render
/// object to clip the results of [Layer.find].
///
/// Neither [value] nor [sized] can be null.
RenderAnnotatedRegion({
required T value,
required bool sized,
RenderBox? child,
}) : assert(value != null),
assert(sized != null),
_value = value,
_sized = sized,
super(child);
/// A value which can be retrieved using [Layer.find].
T get value => _value;
T _value;
set value (T newValue) {
if (_value == newValue) {
return;
}
_value = newValue;
markNeedsPaint();
}
/// Whether the render object will pass its [size] to the [AnnotatedRegionLayer].
bool get sized => _sized;
bool _sized;
set sized(bool value) {
if (_sized == value) {
return;
}
_sized = value;
markNeedsPaint();
}
@override
final bool alwaysNeedsCompositing = true;
@override
void paint(PaintingContext context, Offset offset) {
// Annotated region layers are not retained because they do not create engine layers.
final AnnotatedRegionLayer<T> layer = AnnotatedRegionLayer<T>(
value,
size: sized ? size : null,
offset: sized ? offset : null,
);
context.pushLayer(layer, super.paint, offset);
}
}