| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/widgets.dart'; |
| 6 | /// |
| 7 | /// @docImport 'proxy_box.dart'; |
| 8 | library; |
| 9 | |
| 10 | import 'dart:math' as math; |
| 11 | |
| 12 | import 'package:flutter/foundation.dart'; |
| 13 | |
| 14 | import 'box.dart'; |
| 15 | import 'debug.dart'; |
| 16 | import 'debug_overflow_indicator.dart'; |
| 17 | import 'layer.dart'; |
| 18 | import 'layout_helper.dart'; |
| 19 | import 'object.dart'; |
| 20 | import 'stack.dart' show RelativeRect; |
| 21 | |
| 22 | /// Signature for a function that transforms a [BoxConstraints] to another |
| 23 | /// [BoxConstraints]. |
| 24 | /// |
| 25 | /// Used by [RenderConstraintsTransformBox] and [ConstraintsTransformBox]. |
| 26 | /// Typically the caller requires the returned [BoxConstraints] to be |
| 27 | /// [BoxConstraints.isNormalized]. |
| 28 | typedef BoxConstraintsTransform = BoxConstraints Function(BoxConstraints constraints); |
| 29 | |
| 30 | /// Abstract class for one-child-layout render boxes that provide control over |
| 31 | /// the child's position. |
| 32 | abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> { |
| 33 | /// Initializes the [child] property for subclasses. |
| 34 | RenderShiftedBox(RenderBox? child) { |
| 35 | this.child = child; |
| 36 | } |
| 37 | |
| 38 | @override |
| 39 | double computeMinIntrinsicWidth(double height) { |
| 40 | return child?.getMinIntrinsicWidth(height) ?? 0.0; |
| 41 | } |
| 42 | |
| 43 | @override |
| 44 | double computeMaxIntrinsicWidth(double height) { |
| 45 | return child?.getMaxIntrinsicWidth(height) ?? 0.0; |
| 46 | } |
| 47 | |
| 48 | @override |
| 49 | double computeMinIntrinsicHeight(double width) { |
| 50 | return child?.getMinIntrinsicHeight(width) ?? 0.0; |
| 51 | } |
| 52 | |
| 53 | @override |
| 54 | double computeMaxIntrinsicHeight(double width) { |
| 55 | return child?.getMaxIntrinsicHeight(width) ?? 0.0; |
| 56 | } |
| 57 | |
| 58 | @override |
| 59 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| 60 | double? result; |
| 61 | final RenderBox? child = this.child; |
| 62 | assert(!debugNeedsLayout); |
| 63 | if (child != null) { |
| 64 | assert(!child.debugNeedsLayout); |
| 65 | result = child.getDistanceToActualBaseline(baseline); |
| 66 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
| 67 | if (result != null) { |
| 68 | result += childParentData.offset.dy; |
| 69 | } |
| 70 | } else { |
| 71 | result = super.computeDistanceToActualBaseline(baseline); |
| 72 | } |
| 73 | return result; |
| 74 | } |
| 75 | |
| 76 | @override |
| 77 | void paint(PaintingContext context, Offset offset) { |
| 78 | final RenderBox? child = this.child; |
| 79 | if (child != null) { |
| 80 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
| 81 | context.paintChild(child, childParentData.offset + offset); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | @override |
| 86 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
| 87 | final RenderBox? child = this.child; |
| 88 | if (child != null) { |
| 89 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
| 90 | return result.addWithPaintOffset( |
| 91 | offset: childParentData.offset, |
| 92 | position: position, |
| 93 | hitTest: (BoxHitTestResult result, Offset transformed) { |
| 94 | assert(transformed == position - childParentData.offset); |
| 95 | return child.hitTest(result, position: transformed); |
| 96 | }, |
| 97 | ); |
| 98 | } |
| 99 | return false; |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | /// Insets its child by the given padding. |
| 104 | /// |
| 105 | /// When passing layout constraints to its child, padding shrinks the |
| 106 | /// constraints by the given padding, causing the child to layout at a smaller |
| 107 | /// size. Padding then sizes itself to its child's size, inflated by the |
| 108 | /// padding, effectively creating empty space around the child. |
| 109 | class RenderPadding extends RenderShiftedBox { |
| 110 | /// Creates a render object that insets its child. |
| 111 | /// |
| 112 | /// The [padding] argument must have non-negative insets. |
| 113 | RenderPadding({ |
| 114 | required EdgeInsetsGeometry padding, |
| 115 | TextDirection? textDirection, |
| 116 | RenderBox? child, |
| 117 | }) : assert(padding.isNonNegative), |
| 118 | _textDirection = textDirection, |
| 119 | _padding = padding, |
| 120 | super(child); |
| 121 | |
| 122 | EdgeInsets? _resolvedPaddingCache; |
| 123 | EdgeInsets get _resolvedPadding { |
| 124 | final EdgeInsets returnValue = _resolvedPaddingCache ??= padding.resolve(textDirection); |
| 125 | assert(returnValue.isNonNegative); |
| 126 | return returnValue; |
| 127 | } |
| 128 | |
| 129 | void _markNeedResolution() { |
| 130 | _resolvedPaddingCache = null; |
| 131 | markNeedsLayout(); |
| 132 | } |
| 133 | |
| 134 | /// The amount to pad the child in each dimension. |
| 135 | /// |
| 136 | /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] |
| 137 | /// must not be null. |
| 138 | EdgeInsetsGeometry get padding => _padding; |
| 139 | EdgeInsetsGeometry _padding; |
| 140 | set padding(EdgeInsetsGeometry value) { |
| 141 | assert(value.isNonNegative); |
| 142 | if (_padding == value) { |
| 143 | return; |
| 144 | } |
| 145 | _padding = value; |
| 146 | _markNeedResolution(); |
| 147 | } |
| 148 | |
| 149 | /// The text direction with which to resolve [padding]. |
| 150 | /// |
| 151 | /// This may be changed to null, but only after the [padding] has been changed |
| 152 | /// to a value that does not depend on the direction. |
| 153 | TextDirection? get textDirection => _textDirection; |
| 154 | TextDirection? _textDirection; |
| 155 | set textDirection(TextDirection? value) { |
| 156 | if (_textDirection == value) { |
| 157 | return; |
| 158 | } |
| 159 | _textDirection = value; |
| 160 | _markNeedResolution(); |
| 161 | } |
| 162 | |
| 163 | @override |
| 164 | double computeMinIntrinsicWidth(double height) { |
| 165 | final EdgeInsets padding = _resolvedPadding; |
| 166 | if (child != null) { |
| 167 | // Relies on double.infinity absorption. |
| 168 | return child!.getMinIntrinsicWidth(math.max(0.0, height - padding.vertical)) + |
| 169 | padding.horizontal; |
| 170 | } |
| 171 | return padding.horizontal; |
| 172 | } |
| 173 | |
| 174 | @override |
| 175 | double computeMaxIntrinsicWidth(double height) { |
| 176 | final EdgeInsets padding = _resolvedPadding; |
| 177 | if (child != null) { |
| 178 | // Relies on double.infinity absorption. |
| 179 | return child!.getMaxIntrinsicWidth(math.max(0.0, height - padding.vertical)) + |
| 180 | padding.horizontal; |
| 181 | } |
| 182 | return padding.horizontal; |
| 183 | } |
| 184 | |
| 185 | @override |
| 186 | double computeMinIntrinsicHeight(double width) { |
| 187 | final EdgeInsets padding = _resolvedPadding; |
| 188 | if (child != null) { |
| 189 | // Relies on double.infinity absorption. |
| 190 | return child!.getMinIntrinsicHeight(math.max(0.0, width - padding.horizontal)) + |
| 191 | padding.vertical; |
| 192 | } |
| 193 | return padding.vertical; |
| 194 | } |
| 195 | |
| 196 | @override |
| 197 | double computeMaxIntrinsicHeight(double width) { |
| 198 | final EdgeInsets padding = _resolvedPadding; |
| 199 | if (child != null) { |
| 200 | // Relies on double.infinity absorption. |
| 201 | return child!.getMaxIntrinsicHeight(math.max(0.0, width - padding.horizontal)) + |
| 202 | padding.vertical; |
| 203 | } |
| 204 | return padding.vertical; |
| 205 | } |
| 206 | |
| 207 | @override |
| 208 | @protected |
| 209 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 210 | final EdgeInsets padding = _resolvedPadding; |
| 211 | if (child == null) { |
| 212 | return constraints.constrain(Size(padding.horizontal, padding.vertical)); |
| 213 | } |
| 214 | final BoxConstraints innerConstraints = constraints.deflate(padding); |
| 215 | final Size childSize = child!.getDryLayout(innerConstraints); |
| 216 | return constraints.constrain( |
| 217 | Size(padding.horizontal + childSize.width, padding.vertical + childSize.height), |
| 218 | ); |
| 219 | } |
| 220 | |
| 221 | @override |
| 222 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 223 | final RenderBox? child = this.child; |
| 224 | if (child == null) { |
| 225 | return null; |
| 226 | } |
| 227 | final EdgeInsets padding = _resolvedPadding; |
| 228 | final BoxConstraints innerConstraints = constraints.deflate(padding); |
| 229 | final BaselineOffset result = |
| 230 | BaselineOffset(child.getDryBaseline(innerConstraints, baseline)) + padding.top; |
| 231 | return result.offset; |
| 232 | } |
| 233 | |
| 234 | @override |
| 235 | void performLayout() { |
| 236 | final BoxConstraints constraints = this.constraints; |
| 237 | final EdgeInsets padding = _resolvedPadding; |
| 238 | if (child == null) { |
| 239 | size = constraints.constrain(Size(padding.horizontal, padding.vertical)); |
| 240 | return; |
| 241 | } |
| 242 | final BoxConstraints innerConstraints = constraints.deflate(padding); |
| 243 | child!.layout(innerConstraints, parentUsesSize: true); |
| 244 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
| 245 | childParentData.offset = Offset(padding.left, padding.top); |
| 246 | size = constraints.constrain( |
| 247 | Size(padding.horizontal + child!.size.width, padding.vertical + child!.size.height), |
| 248 | ); |
| 249 | } |
| 250 | |
| 251 | @override |
| 252 | void debugPaintSize(PaintingContext context, Offset offset) { |
| 253 | super.debugPaintSize(context, offset); |
| 254 | assert(() { |
| 255 | final Rect outerRect = offset & size; |
| 256 | debugPaintPadding( |
| 257 | context.canvas, |
| 258 | outerRect, |
| 259 | child != null ? _resolvedPaddingCache!.deflateRect(outerRect) : null, |
| 260 | ); |
| 261 | return true; |
| 262 | }()); |
| 263 | } |
| 264 | |
| 265 | @override |
| 266 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 267 | super.debugFillProperties(properties); |
| 268 | properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding' , padding)); |
| 269 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | /// Abstract class for one-child-layout render boxes that use a |
| 274 | /// [AlignmentGeometry] to align their children. |
| 275 | abstract class RenderAligningShiftedBox extends RenderShiftedBox { |
| 276 | /// Initializes member variables for subclasses. |
| 277 | /// |
| 278 | /// The [textDirection] must be non-null if the [alignment] is |
| 279 | /// direction-sensitive. |
| 280 | RenderAligningShiftedBox({ |
| 281 | AlignmentGeometry alignment = Alignment.center, |
| 282 | required TextDirection? textDirection, |
| 283 | RenderBox? child, |
| 284 | }) : _alignment = alignment, |
| 285 | _textDirection = textDirection, |
| 286 | super(child); |
| 287 | |
| 288 | /// The [Alignment] to use for aligning the child. |
| 289 | /// |
| 290 | /// This is the [alignment] resolved against [textDirection]. Subclasses should |
| 291 | /// use [resolvedAlignment] instead of [alignment] directly, for computing the |
| 292 | /// child's offset. |
| 293 | /// |
| 294 | /// The [performLayout] method will be called when the value changes. |
| 295 | @protected |
| 296 | Alignment get resolvedAlignment => _resolvedAlignment ??= alignment.resolve(textDirection); |
| 297 | Alignment? _resolvedAlignment; |
| 298 | |
| 299 | void _markNeedResolution() { |
| 300 | _resolvedAlignment = null; |
| 301 | markNeedsLayout(); |
| 302 | } |
| 303 | |
| 304 | /// How to align the child. |
| 305 | /// |
| 306 | /// The x and y values of the alignment control the horizontal and vertical |
| 307 | /// alignment, respectively. An x value of -1.0 means that the left edge of |
| 308 | /// the child is aligned with the left edge of the parent whereas an x value |
| 309 | /// of 1.0 means that the right edge of the child is aligned with the right |
| 310 | /// edge of the parent. Other values interpolate (and extrapolate) linearly. |
| 311 | /// For example, a value of 0.0 means that the center of the child is aligned |
| 312 | /// with the center of the parent. |
| 313 | /// |
| 314 | /// If this is set to an [AlignmentDirectional] object, then |
| 315 | /// [textDirection] must not be null. |
| 316 | AlignmentGeometry get alignment => _alignment; |
| 317 | AlignmentGeometry _alignment; |
| 318 | |
| 319 | /// Sets the alignment to a new value, and triggers a layout update. |
| 320 | set alignment(AlignmentGeometry value) { |
| 321 | if (_alignment == value) { |
| 322 | return; |
| 323 | } |
| 324 | _alignment = value; |
| 325 | _markNeedResolution(); |
| 326 | } |
| 327 | |
| 328 | /// The text direction with which to resolve [alignment]. |
| 329 | /// |
| 330 | /// This may be changed to null, but only after [alignment] has been changed |
| 331 | /// to a value that does not depend on the direction. |
| 332 | TextDirection? get textDirection => _textDirection; |
| 333 | TextDirection? _textDirection; |
| 334 | set textDirection(TextDirection? value) { |
| 335 | if (_textDirection == value) { |
| 336 | return; |
| 337 | } |
| 338 | _textDirection = value; |
| 339 | _markNeedResolution(); |
| 340 | } |
| 341 | |
| 342 | /// Apply the current [alignment] to the [child]. |
| 343 | /// |
| 344 | /// Subclasses should call this method if they have a child, to have |
| 345 | /// this class perform the actual alignment. If there is no child, |
| 346 | /// do not call this method. |
| 347 | /// |
| 348 | /// This method must be called after the child has been laid out and |
| 349 | /// this object's own size has been set. |
| 350 | @protected |
| 351 | void alignChild() { |
| 352 | assert(child != null); |
| 353 | assert(!child!.debugNeedsLayout); |
| 354 | assert(child!.hasSize); |
| 355 | assert(hasSize); |
| 356 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
| 357 | childParentData.offset = resolvedAlignment.alongOffset(size - child!.size as Offset); |
| 358 | } |
| 359 | |
| 360 | @override |
| 361 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 362 | super.debugFillProperties(properties); |
| 363 | properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment' , alignment)); |
| 364 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection, defaultValue: null)); |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | /// Positions its child using an [AlignmentGeometry]. |
| 369 | /// |
| 370 | /// For example, to align a box at the bottom right, you would pass this box a |
| 371 | /// tight constraint that is bigger than the child's natural size, |
| 372 | /// with an alignment of [Alignment.bottomRight]. |
| 373 | /// |
| 374 | /// By default, sizes to be as big as possible in both axes. If either axis is |
| 375 | /// unconstrained, then in that direction it will be sized to fit the child's |
| 376 | /// dimensions. Using widthFactor and heightFactor you can force this latter |
| 377 | /// behavior in all cases. |
| 378 | class RenderPositionedBox extends RenderAligningShiftedBox { |
| 379 | /// Creates a render object that positions its child. |
| 380 | RenderPositionedBox({ |
| 381 | super.child, |
| 382 | double? widthFactor, |
| 383 | double? heightFactor, |
| 384 | super.alignment, |
| 385 | super.textDirection, |
| 386 | }) : assert(widthFactor == null || widthFactor >= 0.0), |
| 387 | assert(heightFactor == null || heightFactor >= 0.0), |
| 388 | _widthFactor = widthFactor, |
| 389 | _heightFactor = heightFactor; |
| 390 | |
| 391 | /// If non-null, sets its width to the child's width multiplied by this factor. |
| 392 | /// |
| 393 | /// Can be both greater and less than 1.0 but must be positive. |
| 394 | double? get widthFactor => _widthFactor; |
| 395 | double? _widthFactor; |
| 396 | set widthFactor(double? value) { |
| 397 | assert(value == null || value >= 0.0); |
| 398 | if (_widthFactor == value) { |
| 399 | return; |
| 400 | } |
| 401 | _widthFactor = value; |
| 402 | markNeedsLayout(); |
| 403 | } |
| 404 | |
| 405 | /// If non-null, sets its height to the child's height multiplied by this factor. |
| 406 | /// |
| 407 | /// Can be both greater and less than 1.0 but must be positive. |
| 408 | double? get heightFactor => _heightFactor; |
| 409 | double? _heightFactor; |
| 410 | set heightFactor(double? value) { |
| 411 | assert(value == null || value >= 0.0); |
| 412 | if (_heightFactor == value) { |
| 413 | return; |
| 414 | } |
| 415 | _heightFactor = value; |
| 416 | markNeedsLayout(); |
| 417 | } |
| 418 | |
| 419 | @override |
| 420 | double computeMinIntrinsicWidth(double height) { |
| 421 | return super.computeMinIntrinsicWidth(height) * (_widthFactor ?? 1); |
| 422 | } |
| 423 | |
| 424 | @override |
| 425 | double computeMaxIntrinsicWidth(double height) { |
| 426 | return super.computeMaxIntrinsicWidth(height) * (_widthFactor ?? 1); |
| 427 | } |
| 428 | |
| 429 | @override |
| 430 | double computeMinIntrinsicHeight(double width) { |
| 431 | return super.computeMinIntrinsicHeight(width) * (_heightFactor ?? 1); |
| 432 | } |
| 433 | |
| 434 | @override |
| 435 | double computeMaxIntrinsicHeight(double width) { |
| 436 | return super.computeMaxIntrinsicHeight(width) * (_heightFactor ?? 1); |
| 437 | } |
| 438 | |
| 439 | @override |
| 440 | @protected |
| 441 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 442 | final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity; |
| 443 | final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity; |
| 444 | if (child != null) { |
| 445 | final Size childSize = child!.getDryLayout(constraints.loosen()); |
| 446 | return constraints.constrain( |
| 447 | Size( |
| 448 | shrinkWrapWidth ? childSize.width * (_widthFactor ?? 1.0) : double.infinity, |
| 449 | shrinkWrapHeight ? childSize.height * (_heightFactor ?? 1.0) : double.infinity, |
| 450 | ), |
| 451 | ); |
| 452 | } |
| 453 | return constraints.constrain( |
| 454 | Size(shrinkWrapWidth ? 0.0 : double.infinity, shrinkWrapHeight ? 0.0 : double.infinity), |
| 455 | ); |
| 456 | } |
| 457 | |
| 458 | @override |
| 459 | void performLayout() { |
| 460 | final BoxConstraints constraints = this.constraints; |
| 461 | final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity; |
| 462 | final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity; |
| 463 | |
| 464 | if (child != null) { |
| 465 | child!.layout(constraints.loosen(), parentUsesSize: true); |
| 466 | size = constraints.constrain( |
| 467 | Size( |
| 468 | shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity, |
| 469 | shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity, |
| 470 | ), |
| 471 | ); |
| 472 | alignChild(); |
| 473 | } else { |
| 474 | size = constraints.constrain( |
| 475 | Size(shrinkWrapWidth ? 0.0 : double.infinity, shrinkWrapHeight ? 0.0 : double.infinity), |
| 476 | ); |
| 477 | } |
| 478 | } |
| 479 | |
| 480 | @override |
| 481 | void debugPaintSize(PaintingContext context, Offset offset) { |
| 482 | super.debugPaintSize(context, offset); |
| 483 | assert(() { |
| 484 | final Paint paint; |
| 485 | if (child != null && !child!.size.isEmpty) { |
| 486 | final Path path; |
| 487 | paint = Paint() |
| 488 | ..style = PaintingStyle.stroke |
| 489 | ..strokeWidth = 1.0 |
| 490 | ..color = const Color(0xFFFFFF00); |
| 491 | path = Path(); |
| 492 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
| 493 | if (childParentData.offset.dy > 0.0) { |
| 494 | // vertical alignment arrows |
| 495 | final double headSize = math.min(childParentData.offset.dy * 0.2, 10.0); |
| 496 | path |
| 497 | ..moveTo(offset.dx + size.width / 2.0, offset.dy) |
| 498 | ..relativeLineTo(0.0, childParentData.offset.dy - headSize) |
| 499 | ..relativeLineTo(headSize, 0.0) |
| 500 | ..relativeLineTo(-headSize, headSize) |
| 501 | ..relativeLineTo(-headSize, -headSize) |
| 502 | ..relativeLineTo(headSize, 0.0) |
| 503 | ..moveTo(offset.dx + size.width / 2.0, offset.dy + size.height) |
| 504 | ..relativeLineTo(0.0, -childParentData.offset.dy + headSize) |
| 505 | ..relativeLineTo(headSize, 0.0) |
| 506 | ..relativeLineTo(-headSize, -headSize) |
| 507 | ..relativeLineTo(-headSize, headSize) |
| 508 | ..relativeLineTo(headSize, 0.0); |
| 509 | context.canvas.drawPath(path, paint); |
| 510 | } |
| 511 | if (childParentData.offset.dx > 0.0) { |
| 512 | // horizontal alignment arrows |
| 513 | final double headSize = math.min(childParentData.offset.dx * 0.2, 10.0); |
| 514 | path |
| 515 | ..moveTo(offset.dx, offset.dy + size.height / 2.0) |
| 516 | ..relativeLineTo(childParentData.offset.dx - headSize, 0.0) |
| 517 | ..relativeLineTo(0.0, headSize) |
| 518 | ..relativeLineTo(headSize, -headSize) |
| 519 | ..relativeLineTo(-headSize, -headSize) |
| 520 | ..relativeLineTo(0.0, headSize) |
| 521 | ..moveTo(offset.dx + size.width, offset.dy + size.height / 2.0) |
| 522 | ..relativeLineTo(-childParentData.offset.dx + headSize, 0.0) |
| 523 | ..relativeLineTo(0.0, headSize) |
| 524 | ..relativeLineTo(-headSize, -headSize) |
| 525 | ..relativeLineTo(headSize, -headSize) |
| 526 | ..relativeLineTo(0.0, headSize); |
| 527 | context.canvas.drawPath(path, paint); |
| 528 | } |
| 529 | } else { |
| 530 | paint = Paint()..color = const Color(0x90909090); |
| 531 | context.canvas.drawRect(offset & size, paint); |
| 532 | } |
| 533 | return true; |
| 534 | }()); |
| 535 | } |
| 536 | |
| 537 | @override |
| 538 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 539 | super.debugFillProperties(properties); |
| 540 | properties.add(DoubleProperty('widthFactor' , _widthFactor, ifNull: 'expand' )); |
| 541 | properties.add(DoubleProperty('heightFactor' , _heightFactor, ifNull: 'expand' )); |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | /// How much space should be occupied by the [OverflowBox] if there is no |
| 546 | /// overflow. |
| 547 | enum OverflowBoxFit { |
| 548 | /// The widget will size itself to be as large as the parent allows. |
| 549 | max, |
| 550 | |
| 551 | /// The widget will follow the child's size. |
| 552 | /// |
| 553 | /// More specifically, the render object will size itself to match the size of |
| 554 | /// its child within the constraints of its parent, or as small as the |
| 555 | /// parent allows if no child is set. |
| 556 | deferToChild, |
| 557 | } |
| 558 | |
| 559 | /// A render object that imposes different constraints on its child than it gets |
| 560 | /// from its parent, possibly allowing the child to overflow the parent. |
| 561 | /// |
| 562 | /// A render overflow box proxies most functions in the render box protocol to |
| 563 | /// its child, except that when laying out its child, it passes constraints |
| 564 | /// based on the minWidth, maxWidth, minHeight, and maxHeight fields instead of |
| 565 | /// just passing the parent's constraints in. Specifically, it overrides any of |
| 566 | /// the equivalent fields on the constraints given by the parent with the |
| 567 | /// constraints given by these fields for each such field that is not null. It |
| 568 | /// then sizes itself based on the parent's constraints' maxWidth and maxHeight, |
| 569 | /// ignoring the child's dimensions. |
| 570 | /// |
| 571 | /// For example, if you wanted a box to always render 50 pixels high, regardless |
| 572 | /// of where it was rendered, you would wrap it in a |
| 573 | /// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0. |
| 574 | /// Generally speaking, to avoid confusing behavior around hit testing, a |
| 575 | /// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect. |
| 576 | /// |
| 577 | /// The child is positioned according to [alignment]. To position a smaller |
| 578 | /// child inside a larger parent, use [RenderPositionedBox] and |
| 579 | /// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox. |
| 580 | /// |
| 581 | /// See also: |
| 582 | /// |
| 583 | /// * [RenderConstraintsTransformBox] for a render object that applies an |
| 584 | /// arbitrary transform to its constraints before sizing its child using |
| 585 | /// the new constraints, treating any overflow as error. |
| 586 | /// * [RenderSizedOverflowBox], a render object that is a specific size but |
| 587 | /// passes its original constraints through to its child, which it allows to |
| 588 | /// overflow. |
| 589 | class RenderConstrainedOverflowBox extends RenderAligningShiftedBox { |
| 590 | /// Creates a render object that lets its child overflow itself. |
| 591 | RenderConstrainedOverflowBox({ |
| 592 | super.child, |
| 593 | double? minWidth, |
| 594 | double? maxWidth, |
| 595 | double? minHeight, |
| 596 | double? maxHeight, |
| 597 | OverflowBoxFit fit = OverflowBoxFit.max, |
| 598 | super.alignment, |
| 599 | super.textDirection, |
| 600 | }) : _minWidth = minWidth, |
| 601 | _maxWidth = maxWidth, |
| 602 | _minHeight = minHeight, |
| 603 | _maxHeight = maxHeight, |
| 604 | _fit = fit; |
| 605 | |
| 606 | /// The minimum width constraint to give the child. Set this to null (the |
| 607 | /// default) to use the constraint from the parent instead. |
| 608 | double? get minWidth => _minWidth; |
| 609 | double? _minWidth; |
| 610 | set minWidth(double? value) { |
| 611 | if (_minWidth == value) { |
| 612 | return; |
| 613 | } |
| 614 | _minWidth = value; |
| 615 | markNeedsLayout(); |
| 616 | } |
| 617 | |
| 618 | /// The maximum width constraint to give the child. Set this to null (the |
| 619 | /// default) to use the constraint from the parent instead. |
| 620 | double? get maxWidth => _maxWidth; |
| 621 | double? _maxWidth; |
| 622 | set maxWidth(double? value) { |
| 623 | if (_maxWidth == value) { |
| 624 | return; |
| 625 | } |
| 626 | _maxWidth = value; |
| 627 | markNeedsLayout(); |
| 628 | } |
| 629 | |
| 630 | /// The minimum height constraint to give the child. Set this to null (the |
| 631 | /// default) to use the constraint from the parent instead. |
| 632 | double? get minHeight => _minHeight; |
| 633 | double? _minHeight; |
| 634 | set minHeight(double? value) { |
| 635 | if (_minHeight == value) { |
| 636 | return; |
| 637 | } |
| 638 | _minHeight = value; |
| 639 | markNeedsLayout(); |
| 640 | } |
| 641 | |
| 642 | /// The maximum height constraint to give the child. Set this to null (the |
| 643 | /// default) to use the constraint from the parent instead. |
| 644 | double? get maxHeight => _maxHeight; |
| 645 | double? _maxHeight; |
| 646 | set maxHeight(double? value) { |
| 647 | if (_maxHeight == value) { |
| 648 | return; |
| 649 | } |
| 650 | _maxHeight = value; |
| 651 | markNeedsLayout(); |
| 652 | } |
| 653 | |
| 654 | /// The way to size the render object. |
| 655 | /// |
| 656 | /// This only affects scenario when the child does not indeed overflow. |
| 657 | /// If set to [OverflowBoxFit.deferToChild], the render object will size |
| 658 | /// itself to match the size of its child within the constraints of its |
| 659 | /// parent, or as small as the parent allows if no child is set. |
| 660 | /// If set to [OverflowBoxFit.max] (the default), the |
| 661 | /// render object will size itself to be as large as the parent allows. |
| 662 | OverflowBoxFit get fit => _fit; |
| 663 | OverflowBoxFit _fit; |
| 664 | set fit(OverflowBoxFit value) { |
| 665 | if (_fit == value) { |
| 666 | return; |
| 667 | } |
| 668 | _fit = value; |
| 669 | markNeedsLayoutForSizedByParentChange(); |
| 670 | } |
| 671 | |
| 672 | BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
| 673 | return BoxConstraints( |
| 674 | minWidth: _minWidth ?? constraints.minWidth, |
| 675 | maxWidth: _maxWidth ?? constraints.maxWidth, |
| 676 | minHeight: _minHeight ?? constraints.minHeight, |
| 677 | maxHeight: _maxHeight ?? constraints.maxHeight, |
| 678 | ); |
| 679 | } |
| 680 | |
| 681 | @override |
| 682 | bool get sizedByParent => switch (fit) { |
| 683 | OverflowBoxFit.max => true, |
| 684 | // If deferToChild, the size will be as small as its child when non-overflowing, |
| 685 | // thus it cannot be sizedByParent. |
| 686 | OverflowBoxFit.deferToChild => false, |
| 687 | }; |
| 688 | |
| 689 | @override |
| 690 | @protected |
| 691 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 692 | return switch (fit) { |
| 693 | OverflowBoxFit.max => constraints.biggest, |
| 694 | OverflowBoxFit.deferToChild => child?.getDryLayout(constraints) ?? constraints.smallest, |
| 695 | }; |
| 696 | } |
| 697 | |
| 698 | @override |
| 699 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 700 | final RenderBox? child = this.child; |
| 701 | if (child == null) { |
| 702 | return null; |
| 703 | } |
| 704 | final BoxConstraints childConstraints = _getInnerConstraints(constraints); |
| 705 | final double? result = child.getDryBaseline(childConstraints, baseline); |
| 706 | if (result == null) { |
| 707 | return null; |
| 708 | } |
| 709 | final Size childSize = child.getDryLayout(childConstraints); |
| 710 | final Size size = getDryLayout(constraints); |
| 711 | return result + resolvedAlignment.alongOffset(size - childSize as Offset).dy; |
| 712 | } |
| 713 | |
| 714 | @override |
| 715 | void performLayout() { |
| 716 | if (child != null) { |
| 717 | child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
| 718 | switch (fit) { |
| 719 | case OverflowBoxFit.max: |
| 720 | assert(sizedByParent); |
| 721 | case OverflowBoxFit.deferToChild: |
| 722 | size = constraints.constrain(child!.size); |
| 723 | } |
| 724 | alignChild(); |
| 725 | } else { |
| 726 | switch (fit) { |
| 727 | case OverflowBoxFit.max: |
| 728 | assert(sizedByParent); |
| 729 | case OverflowBoxFit.deferToChild: |
| 730 | size = constraints.smallest; |
| 731 | } |
| 732 | } |
| 733 | } |
| 734 | |
| 735 | @override |
| 736 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 737 | super.debugFillProperties(properties); |
| 738 | properties.add(DoubleProperty('minWidth' , minWidth, ifNull: 'use parent minWidth constraint' )); |
| 739 | properties.add(DoubleProperty('maxWidth' , maxWidth, ifNull: 'use parent maxWidth constraint' )); |
| 740 | properties.add( |
| 741 | DoubleProperty('minHeight' , minHeight, ifNull: 'use parent minHeight constraint' ), |
| 742 | ); |
| 743 | properties.add( |
| 744 | DoubleProperty('maxHeight' , maxHeight, ifNull: 'use parent maxHeight constraint' ), |
| 745 | ); |
| 746 | properties.add(EnumProperty<OverflowBoxFit>('fit' , fit)); |
| 747 | } |
| 748 | } |
| 749 | |
| 750 | /// A [RenderBox] that applies an arbitrary transform to its constraints, |
| 751 | /// and sizes its child using the resulting [BoxConstraints], optionally |
| 752 | /// clipping, or treating the overflow as an error. |
| 753 | /// |
| 754 | /// This [RenderBox] sizes its child using a [BoxConstraints] created by |
| 755 | /// applying [constraintsTransform] to this [RenderBox]'s own [constraints]. |
| 756 | /// This box will then attempt to adopt the same size, within the limits of its |
| 757 | /// own constraints. If it ends up with a different size, it will align the |
| 758 | /// child based on [alignment]. If the box cannot expand enough to accommodate |
| 759 | /// the entire child, the child will be clipped if [clipBehavior] is not |
| 760 | /// [Clip.none]. |
| 761 | /// |
| 762 | /// In debug mode, if [clipBehavior] is [Clip.none] and the child overflows the |
| 763 | /// container, a warning will be printed on the console, and black and yellow |
| 764 | /// striped areas will appear where the overflow occurs. |
| 765 | /// |
| 766 | /// When [child] is null, this [RenderBox] takes the smallest possible size and |
| 767 | /// never overflows. |
| 768 | /// |
| 769 | /// This [RenderBox] can be used to ensure some of [child]'s natural dimensions |
| 770 | /// are honored, and get an early warning during development otherwise. For |
| 771 | /// instance, if [child] requires a minimum height to fully display its content, |
| 772 | /// [constraintsTransform] can be set to a function that removes the `maxHeight` |
| 773 | /// constraint from the incoming [BoxConstraints], so that if the parent |
| 774 | /// [RenderObject] fails to provide enough vertical space, a warning will be |
| 775 | /// displayed in debug mode, while still allowing [child] to grow vertically. |
| 776 | /// |
| 777 | /// See also: |
| 778 | /// |
| 779 | /// * [ConstraintsTransformBox], the widget that makes use of this |
| 780 | /// [RenderObject] and exposes the same functionality. |
| 781 | /// * [RenderConstrainedBox], which renders a box which imposes constraints |
| 782 | /// on its child. |
| 783 | /// * [RenderConstrainedOverflowBox], which renders a box that imposes different |
| 784 | /// constraints on its child than it gets from its parent, possibly allowing |
| 785 | /// the child to overflow the parent. |
| 786 | /// * [RenderConstraintsTransformBox] for a render object that applies an |
| 787 | /// arbitrary transform to its constraints before sizing its child using |
| 788 | /// the new constraints, treating any overflow as error. |
| 789 | class RenderConstraintsTransformBox extends RenderAligningShiftedBox |
| 790 | with DebugOverflowIndicatorMixin { |
| 791 | /// Creates a [RenderBox] that sizes itself to the child and modifies the |
| 792 | /// [constraints] before passing it down to that child. |
| 793 | RenderConstraintsTransformBox({ |
| 794 | required super.alignment, |
| 795 | required super.textDirection, |
| 796 | required BoxConstraintsTransform constraintsTransform, |
| 797 | super.child, |
| 798 | Clip clipBehavior = Clip.none, |
| 799 | }) : _constraintsTransform = constraintsTransform, |
| 800 | _clipBehavior = clipBehavior; |
| 801 | |
| 802 | /// {@macro flutter.widgets.constraintsTransform} |
| 803 | BoxConstraintsTransform get constraintsTransform => _constraintsTransform; |
| 804 | BoxConstraintsTransform _constraintsTransform; |
| 805 | set constraintsTransform(BoxConstraintsTransform value) { |
| 806 | if (_constraintsTransform == value) { |
| 807 | return; |
| 808 | } |
| 809 | _constraintsTransform = value; |
| 810 | // The RenderObject only needs layout if the new transform maps the current |
| 811 | // `constraints` to a different value, or the render object has never been |
| 812 | // laid out before. |
| 813 | final bool needsLayout = _childConstraints == null || _childConstraints != value(constraints); |
| 814 | if (needsLayout) { |
| 815 | markNeedsLayout(); |
| 816 | } |
| 817 | } |
| 818 | |
| 819 | /// {@macro flutter.material.Material.clipBehavior} |
| 820 | /// |
| 821 | /// {@macro flutter.widgets.ConstraintsTransformBox.clipBehavior} |
| 822 | /// |
| 823 | /// Defaults to [Clip.none]. |
| 824 | Clip get clipBehavior => _clipBehavior; |
| 825 | Clip _clipBehavior; |
| 826 | set clipBehavior(Clip value) { |
| 827 | if (value != _clipBehavior) { |
| 828 | _clipBehavior = value; |
| 829 | markNeedsPaint(); |
| 830 | markNeedsSemanticsUpdate(); |
| 831 | } |
| 832 | } |
| 833 | |
| 834 | @override |
| 835 | double computeMinIntrinsicHeight(double width) { |
| 836 | return super.computeMinIntrinsicHeight( |
| 837 | constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth, |
| 838 | ); |
| 839 | } |
| 840 | |
| 841 | @override |
| 842 | double computeMaxIntrinsicHeight(double width) { |
| 843 | return super.computeMaxIntrinsicHeight( |
| 844 | constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth, |
| 845 | ); |
| 846 | } |
| 847 | |
| 848 | @override |
| 849 | double computeMinIntrinsicWidth(double height) { |
| 850 | return super.computeMinIntrinsicWidth( |
| 851 | constraintsTransform(BoxConstraints(maxHeight: height)).maxHeight, |
| 852 | ); |
| 853 | } |
| 854 | |
| 855 | @override |
| 856 | double computeMaxIntrinsicWidth(double height) { |
| 857 | return super.computeMaxIntrinsicWidth( |
| 858 | constraintsTransform(BoxConstraints(maxHeight: height)).maxHeight, |
| 859 | ); |
| 860 | } |
| 861 | |
| 862 | @override |
| 863 | @protected |
| 864 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 865 | final Size? childSize = child?.getDryLayout(constraintsTransform(constraints)); |
| 866 | return childSize == null ? constraints.smallest : constraints.constrain(childSize); |
| 867 | } |
| 868 | |
| 869 | @override |
| 870 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 871 | final RenderBox? child = this.child; |
| 872 | if (child == null) { |
| 873 | return null; |
| 874 | } |
| 875 | final BoxConstraints childConstraints = constraintsTransform(constraints); |
| 876 | final double? result = child.getDryBaseline(childConstraints, baseline); |
| 877 | if (result == null) { |
| 878 | return null; |
| 879 | } |
| 880 | final Size childSize = child.getDryLayout(childConstraints); |
| 881 | final Size size = constraints.constrain(childSize); |
| 882 | return result + resolvedAlignment.alongOffset(size - childSize as Offset).dy; |
| 883 | } |
| 884 | |
| 885 | Rect _overflowContainerRect = Rect.zero; |
| 886 | Rect _overflowChildRect = Rect.zero; |
| 887 | bool _isOverflowing = false; |
| 888 | |
| 889 | BoxConstraints? _childConstraints; |
| 890 | |
| 891 | @override |
| 892 | void performLayout() { |
| 893 | final BoxConstraints constraints = this.constraints; |
| 894 | final RenderBox? child = this.child; |
| 895 | if (child != null) { |
| 896 | final BoxConstraints childConstraints = constraintsTransform(constraints); |
| 897 | assert(childConstraints.isNormalized, ' $childConstraints is not normalized' ); |
| 898 | _childConstraints = childConstraints; |
| 899 | child.layout(childConstraints, parentUsesSize: true); |
| 900 | size = constraints.constrain(child.size); |
| 901 | alignChild(); |
| 902 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
| 903 | _overflowContainerRect = Offset.zero & size; |
| 904 | _overflowChildRect = childParentData.offset & child.size; |
| 905 | } else { |
| 906 | size = constraints.smallest; |
| 907 | _overflowContainerRect = Rect.zero; |
| 908 | _overflowChildRect = Rect.zero; |
| 909 | } |
| 910 | _isOverflowing = RelativeRect.fromRect(_overflowContainerRect, _overflowChildRect).hasInsets; |
| 911 | } |
| 912 | |
| 913 | @override |
| 914 | void paint(PaintingContext context, Offset offset) { |
| 915 | if (child == null) { |
| 916 | return; |
| 917 | } |
| 918 | |
| 919 | if (!_isOverflowing) { |
| 920 | super.paint(context, offset); |
| 921 | return; |
| 922 | } |
| 923 | |
| 924 | // We have overflow and the clipBehavior isn't none. Clip it. |
| 925 | _clipRectLayer.layer = context.pushClipRect( |
| 926 | needsCompositing, |
| 927 | offset, |
| 928 | Offset.zero & size, |
| 929 | super.paint, |
| 930 | clipBehavior: clipBehavior, |
| 931 | oldLayer: _clipRectLayer.layer, |
| 932 | ); |
| 933 | |
| 934 | // Display the overflow indicator if clipBehavior is Clip.none. |
| 935 | assert(() { |
| 936 | if (size.isEmpty) { |
| 937 | return true; |
| 938 | } |
| 939 | switch (clipBehavior) { |
| 940 | case Clip.none: |
| 941 | paintOverflowIndicator(context, offset, _overflowContainerRect, _overflowChildRect); |
| 942 | case Clip.hardEdge: |
| 943 | case Clip.antiAlias: |
| 944 | case Clip.antiAliasWithSaveLayer: |
| 945 | break; |
| 946 | } |
| 947 | return true; |
| 948 | }()); |
| 949 | } |
| 950 | |
| 951 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
| 952 | |
| 953 | @override |
| 954 | void dispose() { |
| 955 | _clipRectLayer.layer = null; |
| 956 | super.dispose(); |
| 957 | } |
| 958 | |
| 959 | @override |
| 960 | Rect? describeApproximatePaintClip(RenderObject child) { |
| 961 | switch (clipBehavior) { |
| 962 | case Clip.none: |
| 963 | return null; |
| 964 | case Clip.hardEdge: |
| 965 | case Clip.antiAlias: |
| 966 | case Clip.antiAliasWithSaveLayer: |
| 967 | return _isOverflowing ? Offset.zero & size : null; |
| 968 | } |
| 969 | } |
| 970 | |
| 971 | @override |
| 972 | String toStringShort() { |
| 973 | String header = super.toStringShort(); |
| 974 | if (!kReleaseMode) { |
| 975 | if (_isOverflowing) { |
| 976 | header += ' OVERFLOWING' ; |
| 977 | } |
| 978 | } |
| 979 | return header; |
| 980 | } |
| 981 | } |
| 982 | |
| 983 | /// A render object that is a specific size but passes its original constraints |
| 984 | /// through to its child, which it allows to overflow. |
| 985 | /// |
| 986 | /// If the child's resulting size differs from this render object's size, then |
| 987 | /// the child is aligned according to the [alignment] property. |
| 988 | /// |
| 989 | /// See also: |
| 990 | /// |
| 991 | /// * [RenderConstraintsTransformBox] for a render object that applies an |
| 992 | /// arbitrary transform to its constraints before sizing its child using |
| 993 | /// the new constraints, treating any overflow as error. |
| 994 | /// * [RenderConstrainedOverflowBox] for a render object that imposes |
| 995 | /// different constraints on its child than it gets from its parent, |
| 996 | /// possibly allowing the child to overflow the parent. |
| 997 | class RenderSizedOverflowBox extends RenderAligningShiftedBox { |
| 998 | /// Creates a render box of a given size that lets its child overflow. |
| 999 | /// |
| 1000 | /// The [textDirection] argument must not be null if the [alignment] is |
| 1001 | /// direction-sensitive. |
| 1002 | RenderSizedOverflowBox({ |
| 1003 | super.child, |
| 1004 | required Size requestedSize, |
| 1005 | super.alignment, |
| 1006 | super.textDirection, |
| 1007 | }) : _requestedSize = requestedSize; |
| 1008 | |
| 1009 | /// The size this render box should attempt to be. |
| 1010 | Size get requestedSize => _requestedSize; |
| 1011 | Size _requestedSize; |
| 1012 | set requestedSize(Size value) { |
| 1013 | if (_requestedSize == value) { |
| 1014 | return; |
| 1015 | } |
| 1016 | _requestedSize = value; |
| 1017 | markNeedsLayout(); |
| 1018 | } |
| 1019 | |
| 1020 | @override |
| 1021 | double computeMinIntrinsicWidth(double height) { |
| 1022 | return _requestedSize.width; |
| 1023 | } |
| 1024 | |
| 1025 | @override |
| 1026 | double computeMaxIntrinsicWidth(double height) { |
| 1027 | return _requestedSize.width; |
| 1028 | } |
| 1029 | |
| 1030 | @override |
| 1031 | double computeMinIntrinsicHeight(double width) { |
| 1032 | return _requestedSize.height; |
| 1033 | } |
| 1034 | |
| 1035 | @override |
| 1036 | double computeMaxIntrinsicHeight(double width) { |
| 1037 | return _requestedSize.height; |
| 1038 | } |
| 1039 | |
| 1040 | @override |
| 1041 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| 1042 | return child?.getDistanceToActualBaseline(baseline) ?? |
| 1043 | super.computeDistanceToActualBaseline(baseline); |
| 1044 | } |
| 1045 | |
| 1046 | @override |
| 1047 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 1048 | final RenderBox? child = this.child; |
| 1049 | if (child == null) { |
| 1050 | return null; |
| 1051 | } |
| 1052 | final double? result = child.getDryBaseline(constraints, baseline); |
| 1053 | if (result == null) { |
| 1054 | return null; |
| 1055 | } |
| 1056 | final Size childSize = child.getDryLayout(constraints); |
| 1057 | final Size size = getDryLayout(constraints); |
| 1058 | return result + resolvedAlignment.alongOffset(size - childSize as Offset).dy; |
| 1059 | } |
| 1060 | |
| 1061 | @override |
| 1062 | @protected |
| 1063 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 1064 | return constraints.constrain(_requestedSize); |
| 1065 | } |
| 1066 | |
| 1067 | @override |
| 1068 | void performLayout() { |
| 1069 | size = constraints.constrain(_requestedSize); |
| 1070 | if (child != null) { |
| 1071 | child!.layout(constraints, parentUsesSize: true); |
| 1072 | alignChild(); |
| 1073 | } |
| 1074 | } |
| 1075 | } |
| 1076 | |
| 1077 | /// Sizes its child to a fraction of the total available space. |
| 1078 | /// |
| 1079 | /// For both its width and height, this render object imposes a tight |
| 1080 | /// constraint on its child that is a multiple (typically less than 1.0) of the |
| 1081 | /// maximum constraint it received from its parent on that axis. If the factor |
| 1082 | /// for a given axis is null, then the constraints from the parent are just |
| 1083 | /// passed through instead. |
| 1084 | /// |
| 1085 | /// It then tries to size itself to the size of its child. Where this is not |
| 1086 | /// possible (e.g. if the constraints from the parent are themselves tight), the |
| 1087 | /// child is aligned according to [alignment]. |
| 1088 | class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox { |
| 1089 | /// Creates a render box that sizes its child to a fraction of the total available space. |
| 1090 | /// |
| 1091 | /// If non-null, the [widthFactor] and [heightFactor] arguments must be |
| 1092 | /// non-negative. |
| 1093 | /// |
| 1094 | /// The [textDirection] must be non-null if the [alignment] is |
| 1095 | /// direction-sensitive. |
| 1096 | RenderFractionallySizedOverflowBox({ |
| 1097 | super.child, |
| 1098 | double? widthFactor, |
| 1099 | double? heightFactor, |
| 1100 | super.alignment, |
| 1101 | super.textDirection, |
| 1102 | }) : _widthFactor = widthFactor, |
| 1103 | _heightFactor = heightFactor { |
| 1104 | assert(_widthFactor == null || _widthFactor! >= 0.0); |
| 1105 | assert(_heightFactor == null || _heightFactor! >= 0.0); |
| 1106 | } |
| 1107 | |
| 1108 | /// If non-null, the factor of the incoming width to use. |
| 1109 | /// |
| 1110 | /// If non-null, the child is given a tight width constraint that is the max |
| 1111 | /// incoming width constraint multiplied by this factor. If null, the child is |
| 1112 | /// given the incoming width constraints. |
| 1113 | double? get widthFactor => _widthFactor; |
| 1114 | double? _widthFactor; |
| 1115 | set widthFactor(double? value) { |
| 1116 | assert(value == null || value >= 0.0); |
| 1117 | if (_widthFactor == value) { |
| 1118 | return; |
| 1119 | } |
| 1120 | _widthFactor = value; |
| 1121 | markNeedsLayout(); |
| 1122 | } |
| 1123 | |
| 1124 | /// If non-null, the factor of the incoming height to use. |
| 1125 | /// |
| 1126 | /// If non-null, the child is given a tight height constraint that is the max |
| 1127 | /// incoming width constraint multiplied by this factor. If null, the child is |
| 1128 | /// given the incoming width constraints. |
| 1129 | double? get heightFactor => _heightFactor; |
| 1130 | double? _heightFactor; |
| 1131 | set heightFactor(double? value) { |
| 1132 | assert(value == null || value >= 0.0); |
| 1133 | if (_heightFactor == value) { |
| 1134 | return; |
| 1135 | } |
| 1136 | _heightFactor = value; |
| 1137 | markNeedsLayout(); |
| 1138 | } |
| 1139 | |
| 1140 | BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
| 1141 | double minWidth = constraints.minWidth; |
| 1142 | double maxWidth = constraints.maxWidth; |
| 1143 | if (_widthFactor != null) { |
| 1144 | final double width = maxWidth * _widthFactor!; |
| 1145 | minWidth = width; |
| 1146 | maxWidth = width; |
| 1147 | } |
| 1148 | double minHeight = constraints.minHeight; |
| 1149 | double maxHeight = constraints.maxHeight; |
| 1150 | if (_heightFactor != null) { |
| 1151 | final double height = maxHeight * _heightFactor!; |
| 1152 | minHeight = height; |
| 1153 | maxHeight = height; |
| 1154 | } |
| 1155 | return BoxConstraints( |
| 1156 | minWidth: minWidth, |
| 1157 | maxWidth: maxWidth, |
| 1158 | minHeight: minHeight, |
| 1159 | maxHeight: maxHeight, |
| 1160 | ); |
| 1161 | } |
| 1162 | |
| 1163 | @override |
| 1164 | double computeMinIntrinsicWidth(double height) { |
| 1165 | final double result; |
| 1166 | if (child == null) { |
| 1167 | result = super.computeMinIntrinsicWidth(height); |
| 1168 | } else { |
| 1169 | // the following line relies on double.infinity absorption |
| 1170 | result = child!.getMinIntrinsicWidth(height * (_heightFactor ?? 1.0)); |
| 1171 | } |
| 1172 | assert(result.isFinite); |
| 1173 | return result / (_widthFactor ?? 1.0); |
| 1174 | } |
| 1175 | |
| 1176 | @override |
| 1177 | double computeMaxIntrinsicWidth(double height) { |
| 1178 | final double result; |
| 1179 | if (child == null) { |
| 1180 | result = super.computeMaxIntrinsicWidth(height); |
| 1181 | } else { |
| 1182 | // the following line relies on double.infinity absorption |
| 1183 | result = child!.getMaxIntrinsicWidth(height * (_heightFactor ?? 1.0)); |
| 1184 | } |
| 1185 | assert(result.isFinite); |
| 1186 | return result / (_widthFactor ?? 1.0); |
| 1187 | } |
| 1188 | |
| 1189 | @override |
| 1190 | double computeMinIntrinsicHeight(double width) { |
| 1191 | final double result; |
| 1192 | if (child == null) { |
| 1193 | result = super.computeMinIntrinsicHeight(width); |
| 1194 | } else { |
| 1195 | // the following line relies on double.infinity absorption |
| 1196 | result = child!.getMinIntrinsicHeight(width * (_widthFactor ?? 1.0)); |
| 1197 | } |
| 1198 | assert(result.isFinite); |
| 1199 | return result / (_heightFactor ?? 1.0); |
| 1200 | } |
| 1201 | |
| 1202 | @override |
| 1203 | double computeMaxIntrinsicHeight(double width) { |
| 1204 | final double result; |
| 1205 | if (child == null) { |
| 1206 | result = super.computeMaxIntrinsicHeight(width); |
| 1207 | } else { |
| 1208 | // the following line relies on double.infinity absorption |
| 1209 | result = child!.getMaxIntrinsicHeight(width * (_widthFactor ?? 1.0)); |
| 1210 | } |
| 1211 | assert(result.isFinite); |
| 1212 | return result / (_heightFactor ?? 1.0); |
| 1213 | } |
| 1214 | |
| 1215 | @override |
| 1216 | @protected |
| 1217 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 1218 | if (child != null) { |
| 1219 | final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
| 1220 | return constraints.constrain(childSize); |
| 1221 | } |
| 1222 | return constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero)); |
| 1223 | } |
| 1224 | |
| 1225 | @override |
| 1226 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 1227 | final RenderBox? child = this.child; |
| 1228 | if (child == null) { |
| 1229 | return null; |
| 1230 | } |
| 1231 | final BoxConstraints childConstraints = _getInnerConstraints(constraints); |
| 1232 | final double? result = child.getDryBaseline(childConstraints, baseline); |
| 1233 | if (result == null) { |
| 1234 | return null; |
| 1235 | } |
| 1236 | final Size childSize = child.getDryLayout(childConstraints); |
| 1237 | final Size size = getDryLayout(constraints); |
| 1238 | return result + resolvedAlignment.alongOffset(size - childSize as Offset).dy; |
| 1239 | } |
| 1240 | |
| 1241 | @override |
| 1242 | void performLayout() { |
| 1243 | if (child != null) { |
| 1244 | child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
| 1245 | size = constraints.constrain(child!.size); |
| 1246 | alignChild(); |
| 1247 | } else { |
| 1248 | size = constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero)); |
| 1249 | } |
| 1250 | } |
| 1251 | |
| 1252 | @override |
| 1253 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 1254 | super.debugFillProperties(properties); |
| 1255 | properties.add(DoubleProperty('widthFactor' , _widthFactor, ifNull: 'pass-through' )); |
| 1256 | properties.add(DoubleProperty('heightFactor' , _heightFactor, ifNull: 'pass-through' )); |
| 1257 | } |
| 1258 | } |
| 1259 | |
| 1260 | /// A delegate for computing the layout of a render object with a single child. |
| 1261 | /// |
| 1262 | /// Used by [CustomSingleChildLayout] (in the widgets library) and |
| 1263 | /// [RenderCustomSingleChildLayoutBox] (in the rendering library). |
| 1264 | /// |
| 1265 | /// When asked to layout, [CustomSingleChildLayout] first calls [getSize] with |
| 1266 | /// its incoming constraints to determine its size. It then calls |
| 1267 | /// [getConstraintsForChild] to determine the constraints to apply to the child. |
| 1268 | /// After the child completes its layout, [RenderCustomSingleChildLayoutBox] |
| 1269 | /// calls [getPositionForChild] to determine the child's position. |
| 1270 | /// |
| 1271 | /// The [shouldRelayout] method is called when a new instance of the class |
| 1272 | /// is provided, to check if the new instance actually represents different |
| 1273 | /// information. |
| 1274 | /// |
| 1275 | /// The most efficient way to trigger a relayout is to supply a `relayout` |
| 1276 | /// argument to the constructor of the [SingleChildLayoutDelegate]. The custom |
| 1277 | /// layout will listen to this value and relayout whenever the Listenable |
| 1278 | /// notifies its listeners, such as when an [Animation] ticks. This allows |
| 1279 | /// the custom layout to avoid the build phase of the pipeline. |
| 1280 | /// |
| 1281 | /// See also: |
| 1282 | /// |
| 1283 | /// * [CustomSingleChildLayout], the widget that uses this delegate. |
| 1284 | /// * [RenderCustomSingleChildLayoutBox], render object that uses this |
| 1285 | /// delegate. |
| 1286 | abstract class SingleChildLayoutDelegate { |
| 1287 | /// Creates a layout delegate. |
| 1288 | /// |
| 1289 | /// The layout will update whenever [relayout] notifies its listeners. |
| 1290 | const SingleChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout; |
| 1291 | |
| 1292 | final Listenable? _relayout; |
| 1293 | |
| 1294 | /// The size of this object given the incoming constraints. |
| 1295 | /// |
| 1296 | /// Defaults to the biggest size that satisfies the given constraints. |
| 1297 | Size getSize(BoxConstraints constraints) => constraints.biggest; |
| 1298 | |
| 1299 | /// The constraints for the child given the incoming constraints. |
| 1300 | /// |
| 1301 | /// During layout, the child is given the layout constraints returned by this |
| 1302 | /// function. The child is required to pick a size for itself that satisfies |
| 1303 | /// these constraints. |
| 1304 | /// |
| 1305 | /// Defaults to the given constraints. |
| 1306 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints; |
| 1307 | |
| 1308 | /// The position where the child should be placed. |
| 1309 | /// |
| 1310 | /// The `size` argument is the size of the parent, which might be different |
| 1311 | /// from the value returned by [getSize] if that size doesn't satisfy the |
| 1312 | /// constraints passed to [getSize]. The `childSize` argument is the size of |
| 1313 | /// the child, which will satisfy the constraints returned by |
| 1314 | /// [getConstraintsForChild]. |
| 1315 | /// |
| 1316 | /// Defaults to positioning the child in the upper left corner of the parent. |
| 1317 | Offset getPositionForChild(Size size, Size childSize) => Offset.zero; |
| 1318 | |
| 1319 | /// Called whenever a new instance of the custom layout delegate class is |
| 1320 | /// provided to the [RenderCustomSingleChildLayoutBox] object, or any time |
| 1321 | /// that a new [CustomSingleChildLayout] object is created with a new instance |
| 1322 | /// of the custom layout delegate class (which amounts to the same thing, |
| 1323 | /// because the latter is implemented in terms of the former). |
| 1324 | /// |
| 1325 | /// If the new instance represents different information than the old |
| 1326 | /// instance, then the method should return true, otherwise it should return |
| 1327 | /// false. |
| 1328 | /// |
| 1329 | /// If the method returns false, then the [getSize], |
| 1330 | /// [getConstraintsForChild], and [getPositionForChild] calls might be |
| 1331 | /// optimized away. |
| 1332 | /// |
| 1333 | /// It's possible that the layout methods will get called even if |
| 1334 | /// [shouldRelayout] returns false (e.g. if an ancestor changed its layout). |
| 1335 | /// It's also possible that the layout method will get called |
| 1336 | /// without [shouldRelayout] being called at all (e.g. if the parent changes |
| 1337 | /// size). |
| 1338 | bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate); |
| 1339 | } |
| 1340 | |
| 1341 | /// Defers the layout of its single child to a delegate. |
| 1342 | /// |
| 1343 | /// The delegate can determine the layout constraints for the child and can |
| 1344 | /// decide where to position the child. The delegate can also determine the size |
| 1345 | /// of the parent, but the size of the parent cannot depend on the size of the |
| 1346 | /// child. |
| 1347 | class RenderCustomSingleChildLayoutBox extends RenderShiftedBox { |
| 1348 | /// Creates a render box that defers its layout to a delegate. |
| 1349 | /// |
| 1350 | /// The [delegate] argument must not be null. |
| 1351 | RenderCustomSingleChildLayoutBox({RenderBox? child, required SingleChildLayoutDelegate delegate}) |
| 1352 | : _delegate = delegate, |
| 1353 | super(child); |
| 1354 | |
| 1355 | /// A delegate that controls this object's layout. |
| 1356 | SingleChildLayoutDelegate get delegate => _delegate; |
| 1357 | SingleChildLayoutDelegate _delegate; |
| 1358 | set delegate(SingleChildLayoutDelegate newDelegate) { |
| 1359 | if (_delegate == newDelegate) { |
| 1360 | return; |
| 1361 | } |
| 1362 | final SingleChildLayoutDelegate oldDelegate = _delegate; |
| 1363 | if (newDelegate.runtimeType != oldDelegate.runtimeType || |
| 1364 | newDelegate.shouldRelayout(oldDelegate)) { |
| 1365 | markNeedsLayout(); |
| 1366 | } |
| 1367 | _delegate = newDelegate; |
| 1368 | if (attached) { |
| 1369 | oldDelegate._relayout?.removeListener(markNeedsLayout); |
| 1370 | newDelegate._relayout?.addListener(markNeedsLayout); |
| 1371 | } |
| 1372 | } |
| 1373 | |
| 1374 | @override |
| 1375 | void attach(PipelineOwner owner) { |
| 1376 | super.attach(owner); |
| 1377 | _delegate._relayout?.addListener(markNeedsLayout); |
| 1378 | } |
| 1379 | |
| 1380 | @override |
| 1381 | void detach() { |
| 1382 | _delegate._relayout?.removeListener(markNeedsLayout); |
| 1383 | super.detach(); |
| 1384 | } |
| 1385 | |
| 1386 | Size _getSize(BoxConstraints constraints) { |
| 1387 | return constraints.constrain(_delegate.getSize(constraints)); |
| 1388 | } |
| 1389 | |
| 1390 | // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to |
| 1391 | // figure out the intrinsic dimensions. We really should either not support intrinsics, |
| 1392 | // or we should expose intrinsic delegate callbacks and throw if they're not implemented. |
| 1393 | |
| 1394 | @override |
| 1395 | double computeMinIntrinsicWidth(double height) { |
| 1396 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
| 1397 | if (width.isFinite) { |
| 1398 | return width; |
| 1399 | } |
| 1400 | return 0.0; |
| 1401 | } |
| 1402 | |
| 1403 | @override |
| 1404 | double computeMaxIntrinsicWidth(double height) { |
| 1405 | final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width; |
| 1406 | if (width.isFinite) { |
| 1407 | return width; |
| 1408 | } |
| 1409 | return 0.0; |
| 1410 | } |
| 1411 | |
| 1412 | @override |
| 1413 | double computeMinIntrinsicHeight(double width) { |
| 1414 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
| 1415 | if (height.isFinite) { |
| 1416 | return height; |
| 1417 | } |
| 1418 | return 0.0; |
| 1419 | } |
| 1420 | |
| 1421 | @override |
| 1422 | double computeMaxIntrinsicHeight(double width) { |
| 1423 | final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height; |
| 1424 | if (height.isFinite) { |
| 1425 | return height; |
| 1426 | } |
| 1427 | return 0.0; |
| 1428 | } |
| 1429 | |
| 1430 | @override |
| 1431 | @protected |
| 1432 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 1433 | return _getSize(constraints); |
| 1434 | } |
| 1435 | |
| 1436 | @override |
| 1437 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 1438 | final RenderBox? child = this.child; |
| 1439 | if (child == null) { |
| 1440 | return null; |
| 1441 | } |
| 1442 | final BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints); |
| 1443 | final double? result = child.getDryBaseline(childConstraints, baseline); |
| 1444 | if (result == null) { |
| 1445 | return null; |
| 1446 | } |
| 1447 | return result + |
| 1448 | delegate |
| 1449 | .getPositionForChild( |
| 1450 | _getSize(constraints), |
| 1451 | childConstraints.isTight |
| 1452 | ? childConstraints.smallest |
| 1453 | : child.getDryLayout(childConstraints), |
| 1454 | ) |
| 1455 | .dy; |
| 1456 | } |
| 1457 | |
| 1458 | @override |
| 1459 | void performLayout() { |
| 1460 | size = _getSize(constraints); |
| 1461 | if (child != null) { |
| 1462 | final BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints); |
| 1463 | assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true)); |
| 1464 | child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight); |
| 1465 | final BoxParentData childParentData = child!.parentData! as BoxParentData; |
| 1466 | childParentData.offset = delegate.getPositionForChild( |
| 1467 | size, |
| 1468 | childConstraints.isTight ? childConstraints.smallest : child!.size, |
| 1469 | ); |
| 1470 | } |
| 1471 | } |
| 1472 | } |
| 1473 | |
| 1474 | /// Shifts the child down such that the child's baseline (or the |
| 1475 | /// bottom of the child, if the child has no baseline) is [baseline] |
| 1476 | /// logical pixels below the top of this box, then sizes this box to |
| 1477 | /// contain the child. |
| 1478 | /// |
| 1479 | /// If [baseline] is less than the distance from the top of the child |
| 1480 | /// to the baseline of the child, then the child will overflow the top |
| 1481 | /// of the box. This is typically not desirable, in particular, that |
| 1482 | /// part of the child will not be found when doing hit tests, so the |
| 1483 | /// user cannot interact with that part of the child. |
| 1484 | /// |
| 1485 | /// This box will be sized so that its bottom is coincident with the |
| 1486 | /// bottom of the child. This means if this box shifts the child down, |
| 1487 | /// there will be space between the top of this box and the top of the |
| 1488 | /// child, but there is never space between the bottom of the child |
| 1489 | /// and the bottom of the box. |
| 1490 | class RenderBaseline extends RenderShiftedBox { |
| 1491 | /// Creates a [RenderBaseline] object. |
| 1492 | RenderBaseline({RenderBox? child, required double baseline, required TextBaseline baselineType}) |
| 1493 | : _baseline = baseline, |
| 1494 | _baselineType = baselineType, |
| 1495 | super(child); |
| 1496 | |
| 1497 | /// The number of logical pixels from the top of this box at which to position |
| 1498 | /// the child's baseline. |
| 1499 | double get baseline => _baseline; |
| 1500 | double _baseline; |
| 1501 | set baseline(double value) { |
| 1502 | if (_baseline == value) { |
| 1503 | return; |
| 1504 | } |
| 1505 | _baseline = value; |
| 1506 | markNeedsLayout(); |
| 1507 | } |
| 1508 | |
| 1509 | /// The type of baseline to use for positioning the child. |
| 1510 | TextBaseline get baselineType => _baselineType; |
| 1511 | TextBaseline _baselineType; |
| 1512 | set baselineType(TextBaseline value) { |
| 1513 | if (_baselineType == value) { |
| 1514 | return; |
| 1515 | } |
| 1516 | _baselineType = value; |
| 1517 | markNeedsLayout(); |
| 1518 | } |
| 1519 | |
| 1520 | ({Size size, double top}) _computeSizes( |
| 1521 | covariant BoxConstraints constraints, |
| 1522 | ChildLayouter layoutChild, |
| 1523 | ChildBaselineGetter getBaseline, |
| 1524 | ) { |
| 1525 | final RenderBox? child = this.child; |
| 1526 | if (child == null) { |
| 1527 | return (size: constraints.smallest, top: 0); |
| 1528 | } |
| 1529 | final BoxConstraints childConstraints = constraints.loosen(); |
| 1530 | final Size childSize = layoutChild(child, childConstraints); |
| 1531 | final double childBaseline = |
| 1532 | getBaseline(child, childConstraints, baselineType) ?? childSize.height; |
| 1533 | final double top = baseline - childBaseline; |
| 1534 | return (size: constraints.constrain(Size(childSize.width, top + childSize.height)), top: top); |
| 1535 | } |
| 1536 | |
| 1537 | @override |
| 1538 | @protected |
| 1539 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 1540 | return _computeSizes( |
| 1541 | constraints, |
| 1542 | ChildLayoutHelper.dryLayoutChild, |
| 1543 | ChildLayoutHelper.getDryBaseline, |
| 1544 | ).size; |
| 1545 | } |
| 1546 | |
| 1547 | @override |
| 1548 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
| 1549 | final RenderBox? child = this.child; |
| 1550 | final double? result1 = child?.getDryBaseline(constraints.loosen(), baseline); |
| 1551 | final double? result2 = child?.getDryBaseline(constraints.loosen(), baselineType); |
| 1552 | if (result1 == null || result2 == null) { |
| 1553 | return null; |
| 1554 | } |
| 1555 | return this.baseline + result1 - result2; |
| 1556 | } |
| 1557 | |
| 1558 | @override |
| 1559 | void performLayout() { |
| 1560 | final (:Size size, :double top) = _computeSizes( |
| 1561 | constraints, |
| 1562 | ChildLayoutHelper.layoutChild, |
| 1563 | ChildLayoutHelper.getBaseline, |
| 1564 | ); |
| 1565 | this.size = size; |
| 1566 | (child?.parentData as BoxParentData?)?.offset = Offset(0.0, top); |
| 1567 | } |
| 1568 | |
| 1569 | @override |
| 1570 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 1571 | super.debugFillProperties(properties); |
| 1572 | properties.add(DoubleProperty('baseline' , baseline)); |
| 1573 | properties.add(EnumProperty<TextBaseline>('baselineType' , baselineType)); |
| 1574 | } |
| 1575 | } |
| 1576 | |