| 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 'flow.dart'; |
| 8 | /// @docImport 'proxy_box.dart'; |
| 9 | library; |
| 10 | |
| 11 | import 'dart:math' as math; |
| 12 | import 'dart:ui' show lerpDouble; |
| 13 | |
| 14 | import 'package:flutter/foundation.dart'; |
| 15 | |
| 16 | import 'box.dart'; |
| 17 | import 'layer.dart'; |
| 18 | import 'layout_helper.dart'; |
| 19 | import 'object.dart'; |
| 20 | |
| 21 | /// An immutable 2D, axis-aligned, floating-point rectangle whose coordinates |
| 22 | /// are given relative to another rectangle's edges, known as the container. |
| 23 | /// Since the dimensions of the rectangle are relative to those of the |
| 24 | /// container, this class has no width and height members. To determine the |
| 25 | /// width or height of the rectangle, convert it to a [Rect] using [toRect()] |
| 26 | /// (passing the container's own Rect), and then examine that object. |
| 27 | @immutable |
| 28 | class RelativeRect { |
| 29 | /// Creates a RelativeRect with the given values. |
| 30 | const RelativeRect.fromLTRB(this.left, this.top, this.right, this.bottom); |
| 31 | |
| 32 | /// Creates a RelativeRect from a Rect and a Size. The Rect (first argument) |
| 33 | /// and the RelativeRect (the output) are in the coordinate space of the |
| 34 | /// rectangle described by the Size, with 0,0 being at the top left. |
| 35 | RelativeRect.fromSize(Rect rect, Size container) |
| 36 | : left = rect.left, |
| 37 | top = rect.top, |
| 38 | right = container.width - rect.right, |
| 39 | bottom = container.height - rect.bottom; |
| 40 | |
| 41 | /// Creates a RelativeRect from two Rects. The second Rect provides the |
| 42 | /// container, the first provides the rectangle, in the same coordinate space, |
| 43 | /// that is to be converted to a RelativeRect. The output will be in the |
| 44 | /// container's coordinate space. |
| 45 | /// |
| 46 | /// For example, if the top left of the rect is at 0,0, and the top left of |
| 47 | /// the container is at 100,100, then the top left of the output will be at |
| 48 | /// -100,-100. |
| 49 | /// |
| 50 | /// If the first rect is actually in the container's coordinate space, then |
| 51 | /// use [RelativeRect.fromSize] and pass the container's size as the second |
| 52 | /// argument instead. |
| 53 | RelativeRect.fromRect(Rect rect, Rect container) |
| 54 | : left = rect.left - container.left, |
| 55 | top = rect.top - container.top, |
| 56 | right = container.right - rect.right, |
| 57 | bottom = container.bottom - rect.bottom; |
| 58 | |
| 59 | /// Creates a RelativeRect from horizontal position using `start` and `end` |
| 60 | /// rather than `left` and `right`. |
| 61 | /// |
| 62 | /// If `textDirection` is [TextDirection.rtl], then the `start` argument is |
| 63 | /// used for the [right] property and the `end` argument is used for the |
| 64 | /// [left] property. Otherwise, if `textDirection` is [TextDirection.ltr], |
| 65 | /// then the `start` argument is used for the [left] property and the `end` |
| 66 | /// argument is used for the [right] property. |
| 67 | factory RelativeRect.fromDirectional({ |
| 68 | required TextDirection textDirection, |
| 69 | required double start, |
| 70 | required double top, |
| 71 | required double end, |
| 72 | required double bottom, |
| 73 | }) { |
| 74 | final (double left, double right) = switch (textDirection) { |
| 75 | TextDirection.rtl => (end, start), |
| 76 | TextDirection.ltr => (start, end), |
| 77 | }; |
| 78 | return RelativeRect.fromLTRB(left, top, right, bottom); |
| 79 | } |
| 80 | |
| 81 | /// A rect that covers the entire container. |
| 82 | static const RelativeRect fill = RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0); |
| 83 | |
| 84 | /// Distance from the left side of the container to the left side of this rectangle. |
| 85 | /// |
| 86 | /// May be negative if the left side of the rectangle is outside of the container. |
| 87 | final double left; |
| 88 | |
| 89 | /// Distance from the top side of the container to the top side of this rectangle. |
| 90 | /// |
| 91 | /// May be negative if the top side of the rectangle is outside of the container. |
| 92 | final double top; |
| 93 | |
| 94 | /// Distance from the right side of the container to the right side of this rectangle. |
| 95 | /// |
| 96 | /// May be positive if the right side of the rectangle is outside of the container. |
| 97 | final double right; |
| 98 | |
| 99 | /// Distance from the bottom side of the container to the bottom side of this rectangle. |
| 100 | /// |
| 101 | /// May be positive if the bottom side of the rectangle is outside of the container. |
| 102 | final double bottom; |
| 103 | |
| 104 | /// Returns whether any of the values are greater than zero. |
| 105 | /// |
| 106 | /// This corresponds to one of the sides ([left], [top], [right], or [bottom]) having |
| 107 | /// some positive inset towards the center. |
| 108 | bool get hasInsets => left > 0.0 || top > 0.0 || right > 0.0 || bottom > 0.0; |
| 109 | |
| 110 | /// Returns a new rectangle object translated by the given offset. |
| 111 | RelativeRect shift(Offset offset) { |
| 112 | return RelativeRect.fromLTRB( |
| 113 | left + offset.dx, |
| 114 | top + offset.dy, |
| 115 | right - offset.dx, |
| 116 | bottom - offset.dy, |
| 117 | ); |
| 118 | } |
| 119 | |
| 120 | /// Returns a new rectangle with edges moved outwards by the given delta. |
| 121 | RelativeRect inflate(double delta) { |
| 122 | return RelativeRect.fromLTRB(left - delta, top - delta, right - delta, bottom - delta); |
| 123 | } |
| 124 | |
| 125 | /// Returns a new rectangle with edges moved inwards by the given delta. |
| 126 | RelativeRect deflate(double delta) { |
| 127 | return inflate(-delta); |
| 128 | } |
| 129 | |
| 130 | /// Returns a new rectangle that is the intersection of the given rectangle and this rectangle. |
| 131 | RelativeRect intersect(RelativeRect other) { |
| 132 | return RelativeRect.fromLTRB( |
| 133 | math.max(left, other.left), |
| 134 | math.max(top, other.top), |
| 135 | math.max(right, other.right), |
| 136 | math.max(bottom, other.bottom), |
| 137 | ); |
| 138 | } |
| 139 | |
| 140 | /// Convert this [RelativeRect] to a [Rect], in the coordinate space of the container. |
| 141 | /// |
| 142 | /// See also: |
| 143 | /// |
| 144 | /// * [toSize], which returns the size part of the rect, based on the size of |
| 145 | /// the container. |
| 146 | Rect toRect(Rect container) { |
| 147 | return Rect.fromLTRB(left, top, container.width - right, container.height - bottom); |
| 148 | } |
| 149 | |
| 150 | /// Convert this [RelativeRect] to a [Size], assuming a container with the given size. |
| 151 | /// |
| 152 | /// See also: |
| 153 | /// |
| 154 | /// * [toRect], which also computes the position relative to the container. |
| 155 | Size toSize(Size container) { |
| 156 | return Size(container.width - left - right, container.height - top - bottom); |
| 157 | } |
| 158 | |
| 159 | /// Linearly interpolate between two RelativeRects. |
| 160 | /// |
| 161 | /// If either rect is null, this function interpolates from [RelativeRect.fill]. |
| 162 | /// |
| 163 | /// {@macro dart.ui.shadow.lerp} |
| 164 | static RelativeRect? lerp(RelativeRect? a, RelativeRect? b, double t) { |
| 165 | if (identical(a, b)) { |
| 166 | return a; |
| 167 | } |
| 168 | if (a == null) { |
| 169 | return RelativeRect.fromLTRB(b!.left * t, b.top * t, b.right * t, b.bottom * t); |
| 170 | } |
| 171 | if (b == null) { |
| 172 | final double k = 1.0 - t; |
| 173 | return RelativeRect.fromLTRB(b!.left * k, b.top * k, b.right * k, b.bottom * k); |
| 174 | } |
| 175 | return RelativeRect.fromLTRB( |
| 176 | lerpDouble(a.left, b.left, t)!, |
| 177 | lerpDouble(a.top, b.top, t)!, |
| 178 | lerpDouble(a.right, b.right, t)!, |
| 179 | lerpDouble(a.bottom, b.bottom, t)!, |
| 180 | ); |
| 181 | } |
| 182 | |
| 183 | @override |
| 184 | bool operator ==(Object other) { |
| 185 | if (identical(this, other)) { |
| 186 | return true; |
| 187 | } |
| 188 | return other is RelativeRect && |
| 189 | other.left == left && |
| 190 | other.top == top && |
| 191 | other.right == right && |
| 192 | other.bottom == bottom; |
| 193 | } |
| 194 | |
| 195 | @override |
| 196 | int get hashCode => Object.hash(left, top, right, bottom); |
| 197 | |
| 198 | @override |
| 199 | String toString() => |
| 200 | 'RelativeRect.fromLTRB( ${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)})' ; |
| 201 | } |
| 202 | |
| 203 | /// Parent data for use with [RenderStack]. |
| 204 | class StackParentData extends ContainerBoxParentData<RenderBox> { |
| 205 | /// The distance by which the child's top edge is inset from the top of the stack. |
| 206 | double? top; |
| 207 | |
| 208 | /// The distance by which the child's right edge is inset from the right of the stack. |
| 209 | double? right; |
| 210 | |
| 211 | /// The distance by which the child's bottom edge is inset from the bottom of the stack. |
| 212 | double? bottom; |
| 213 | |
| 214 | /// The distance by which the child's left edge is inset from the left of the stack. |
| 215 | double? left; |
| 216 | |
| 217 | /// The child's width. |
| 218 | /// |
| 219 | /// Ignored if both left and right are non-null. |
| 220 | double? width; |
| 221 | |
| 222 | /// The child's height. |
| 223 | /// |
| 224 | /// Ignored if both top and bottom are non-null. |
| 225 | double? height; |
| 226 | |
| 227 | /// Get or set the current values in terms of a RelativeRect object. |
| 228 | RelativeRect get rect => RelativeRect.fromLTRB(left!, top!, right!, bottom!); |
| 229 | set rect(RelativeRect value) { |
| 230 | top = value.top; |
| 231 | right = value.right; |
| 232 | bottom = value.bottom; |
| 233 | left = value.left; |
| 234 | } |
| 235 | |
| 236 | /// Whether this child is considered positioned. |
| 237 | /// |
| 238 | /// A child is positioned if any of the top, right, bottom, or left properties |
| 239 | /// are non-null. Positioned children do not factor into determining the size |
| 240 | /// of the stack but are instead placed relative to the non-positioned |
| 241 | /// children in the stack. |
| 242 | bool get isPositioned => |
| 243 | top != null || |
| 244 | right != null || |
| 245 | bottom != null || |
| 246 | left != null || |
| 247 | width != null || |
| 248 | height != null; |
| 249 | |
| 250 | /// Computes the [BoxConstraints] the stack layout algorithm would give to |
| 251 | /// this child, given the [Size] of the stack. |
| 252 | /// |
| 253 | /// This method should only be called when [isPositioned] is true for the child. |
| 254 | BoxConstraints positionedChildConstraints(Size stackSize) { |
| 255 | assert(isPositioned); |
| 256 | final double? width = switch ((left, right)) { |
| 257 | (final double left?, final double right?) => stackSize.width - right - left, |
| 258 | (_, _) => this.width, |
| 259 | }; |
| 260 | |
| 261 | final double? height = switch ((top, bottom)) { |
| 262 | (final double top?, final double bottom?) => stackSize.height - bottom - top, |
| 263 | (_, _) => this.height, |
| 264 | }; |
| 265 | assert(height == null || !height.isNaN); |
| 266 | assert(width == null || !width.isNaN); |
| 267 | return BoxConstraints.tightFor( |
| 268 | width: width == null ? null : math.max(0.0, width), |
| 269 | height: height == null ? null : math.max(0.0, height), |
| 270 | ); |
| 271 | } |
| 272 | |
| 273 | @override |
| 274 | String toString() { |
| 275 | final List<String> values = <String>[ |
| 276 | if (top != null) 'top= ${debugFormatDouble(top)}' , |
| 277 | if (right != null) 'right= ${debugFormatDouble(right)}' , |
| 278 | if (bottom != null) 'bottom= ${debugFormatDouble(bottom)}' , |
| 279 | if (left != null) 'left= ${debugFormatDouble(left)}' , |
| 280 | if (width != null) 'width= ${debugFormatDouble(width)}' , |
| 281 | if (height != null) 'height= ${debugFormatDouble(height)}' , |
| 282 | ]; |
| 283 | if (values.isEmpty) { |
| 284 | values.add('not positioned' ); |
| 285 | } |
| 286 | values.add(super.toString()); |
| 287 | return values.join('; ' ); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | /// How to size the non-positioned children of a [Stack]. |
| 292 | /// |
| 293 | /// This enum is used with [Stack.fit] and [RenderStack.fit] to control |
| 294 | /// how the [BoxConstraints] passed from the stack's parent to the stack's child |
| 295 | /// are adjusted. |
| 296 | /// |
| 297 | /// See also: |
| 298 | /// |
| 299 | /// * [Stack], the widget that uses this. |
| 300 | /// * [RenderStack], the render object that implements the stack algorithm. |
| 301 | enum StackFit { |
| 302 | /// The constraints passed to the stack from its parent are loosened. |
| 303 | /// |
| 304 | /// For example, if the stack has constraints that force it to 350x600, then |
| 305 | /// this would allow the non-positioned children of the stack to have any |
| 306 | /// width from zero to 350 and any height from zero to 600. |
| 307 | /// |
| 308 | /// See also: |
| 309 | /// |
| 310 | /// * [Center], which loosens the constraints passed to its child and then |
| 311 | /// centers the child in itself. |
| 312 | /// * [BoxConstraints.loosen], which implements the loosening of box |
| 313 | /// constraints. |
| 314 | loose, |
| 315 | |
| 316 | /// The constraints passed to the stack from its parent are tightened to the |
| 317 | /// biggest size allowed. |
| 318 | /// |
| 319 | /// For example, if the stack has loose constraints with a width in the range |
| 320 | /// 10 to 100 and a height in the range 0 to 600, then the non-positioned |
| 321 | /// children of the stack would all be sized as 100 pixels wide and 600 high. |
| 322 | expand, |
| 323 | |
| 324 | /// The constraints passed to the stack from its parent are passed unmodified |
| 325 | /// to the non-positioned children. |
| 326 | /// |
| 327 | /// For example, if a [Stack] is an [Expanded] child of a [Row], the |
| 328 | /// horizontal constraints will be tight and the vertical constraints will be |
| 329 | /// loose. |
| 330 | passthrough, |
| 331 | } |
| 332 | |
| 333 | /// Implements the stack layout algorithm. |
| 334 | /// |
| 335 | /// In a stack layout, the children are positioned on top of each other in the |
| 336 | /// order in which they appear in the child list. First, the non-positioned |
| 337 | /// children (those with null values for top, right, bottom, and left) are |
| 338 | /// laid out and initially placed in the upper-left corner of the stack. The |
| 339 | /// stack is then sized to enclose all of the non-positioned children. If there |
| 340 | /// are no non-positioned children, the stack becomes as large as possible. |
| 341 | /// |
| 342 | /// The final location of non-positioned children is determined by the alignment |
| 343 | /// parameter. The left of each non-positioned child becomes the |
| 344 | /// difference between the child's width and the stack's width scaled by |
| 345 | /// alignment.x. The top of each non-positioned child is computed |
| 346 | /// similarly and scaled by alignment.y. So if the alignment x and y properties |
| 347 | /// are 0.0 (the default) then the non-positioned children remain in the |
| 348 | /// upper-left corner. If the alignment x and y properties are 0.5 then the |
| 349 | /// non-positioned children are centered within the stack. |
| 350 | /// |
| 351 | /// Next, the positioned children are laid out. If a child has top and bottom |
| 352 | /// values that are both non-null, the child is given a fixed height determined |
| 353 | /// by subtracting the sum of the top and bottom values from the height of the stack. |
| 354 | /// Similarly, if the child has right and left values that are both non-null, |
| 355 | /// the child is given a fixed width derived from the stack's width. |
| 356 | /// Otherwise, the child is given unbounded constraints in the non-fixed dimensions. |
| 357 | /// |
| 358 | /// Once the child is laid out, the stack positions the child |
| 359 | /// according to the top, right, bottom, and left properties of their |
| 360 | /// [StackParentData]. For example, if the bottom value is 10.0, the |
| 361 | /// bottom edge of the child will be inset 10.0 pixels from the bottom |
| 362 | /// edge of the stack. If the child extends beyond the bounds of the |
| 363 | /// stack, the stack will clip the child's painting to the bounds of |
| 364 | /// the stack. |
| 365 | /// |
| 366 | /// See also: |
| 367 | /// |
| 368 | /// * [RenderFlow] |
| 369 | class RenderStack extends RenderBox |
| 370 | with |
| 371 | ContainerRenderObjectMixin<RenderBox, StackParentData>, |
| 372 | RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> { |
| 373 | /// Creates a stack render object. |
| 374 | /// |
| 375 | /// By default, the non-positioned children of the stack are aligned by their |
| 376 | /// top left corners. |
| 377 | RenderStack({ |
| 378 | List<RenderBox>? children, |
| 379 | AlignmentGeometry alignment = AlignmentDirectional.topStart, |
| 380 | TextDirection? textDirection, |
| 381 | StackFit fit = StackFit.loose, |
| 382 | Clip clipBehavior = Clip.hardEdge, |
| 383 | }) : _alignment = alignment, |
| 384 | _textDirection = textDirection, |
| 385 | _fit = fit, |
| 386 | _clipBehavior = clipBehavior { |
| 387 | addAll(children); |
| 388 | } |
| 389 | |
| 390 | bool _hasVisualOverflow = false; |
| 391 | |
| 392 | @override |
| 393 | void setupParentData(RenderBox child) { |
| 394 | if (child.parentData is! StackParentData) { |
| 395 | child.parentData = StackParentData(); |
| 396 | } |
| 397 | } |
| 398 | |
| 399 | Alignment get _resolvedAlignment => _resolvedAlignmentCache ??= alignment.resolve(textDirection); |
| 400 | Alignment? _resolvedAlignmentCache; |
| 401 | |
| 402 | void _markNeedResolution() { |
| 403 | _resolvedAlignmentCache = null; |
| 404 | markNeedsLayout(); |
| 405 | } |
| 406 | |
| 407 | /// How to align the non-positioned or partially-positioned children in the |
| 408 | /// stack. |
| 409 | /// |
| 410 | /// The non-positioned children are placed relative to each other such that |
| 411 | /// the points determined by [alignment] are co-located. For example, if the |
| 412 | /// [alignment] is [Alignment.topLeft], then the top left corner of |
| 413 | /// each non-positioned child will be located at the same global coordinate. |
| 414 | /// |
| 415 | /// Partially-positioned children, those that do not specify an alignment in a |
| 416 | /// particular axis (e.g. that have neither `top` nor `bottom` set), use the |
| 417 | /// alignment to determine how they should be positioned in that |
| 418 | /// under-specified axis. |
| 419 | /// |
| 420 | /// If this is set to an [AlignmentDirectional] object, then [textDirection] |
| 421 | /// must not be null. |
| 422 | AlignmentGeometry get alignment => _alignment; |
| 423 | AlignmentGeometry _alignment; |
| 424 | set alignment(AlignmentGeometry value) { |
| 425 | if (_alignment == value) { |
| 426 | return; |
| 427 | } |
| 428 | _alignment = value; |
| 429 | _markNeedResolution(); |
| 430 | } |
| 431 | |
| 432 | /// The text direction with which to resolve [alignment]. |
| 433 | /// |
| 434 | /// This may be changed to null, but only after the [alignment] has been changed |
| 435 | /// to a value that does not depend on the direction. |
| 436 | TextDirection? get textDirection => _textDirection; |
| 437 | TextDirection? _textDirection; |
| 438 | set textDirection(TextDirection? value) { |
| 439 | if (_textDirection == value) { |
| 440 | return; |
| 441 | } |
| 442 | _textDirection = value; |
| 443 | _markNeedResolution(); |
| 444 | } |
| 445 | |
| 446 | /// How to size the non-positioned children in the stack. |
| 447 | /// |
| 448 | /// The constraints passed into the [RenderStack] from its parent are either |
| 449 | /// loosened ([StackFit.loose]) or tightened to their biggest size |
| 450 | /// ([StackFit.expand]). |
| 451 | StackFit get fit => _fit; |
| 452 | StackFit _fit; |
| 453 | set fit(StackFit value) { |
| 454 | if (_fit != value) { |
| 455 | _fit = value; |
| 456 | markNeedsLayout(); |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | /// {@macro flutter.material.Material.clipBehavior} |
| 461 | /// |
| 462 | /// Stacks only clip children whose geometry overflow the stack. A child that |
| 463 | /// paints outside its bounds (e.g. a box with a shadow) will not be clipped, |
| 464 | /// regardless of the value of this property. Similarly, a child that itself |
| 465 | /// has a descendant that overflows the stack will not be clipped, as only the |
| 466 | /// geometry of the stack's direct children are considered. |
| 467 | /// |
| 468 | /// To clip children whose geometry does not overflow the stack, consider |
| 469 | /// using a [RenderClipRect] render object. |
| 470 | /// |
| 471 | /// Defaults to [Clip.hardEdge]. |
| 472 | Clip get clipBehavior => _clipBehavior; |
| 473 | Clip _clipBehavior = Clip.hardEdge; |
| 474 | set clipBehavior(Clip value) { |
| 475 | if (value != _clipBehavior) { |
| 476 | _clipBehavior = value; |
| 477 | markNeedsPaint(); |
| 478 | markNeedsSemanticsUpdate(); |
| 479 | } |
| 480 | } |
| 481 | |
| 482 | /// Helper function for calculating the intrinsics metrics of a Stack. |
| 483 | static double getIntrinsicDimension( |
| 484 | RenderBox? firstChild, |
| 485 | double Function(RenderBox child) mainChildSizeGetter, |
| 486 | ) { |
| 487 | double extent = 0.0; |
| 488 | RenderBox? child = firstChild; |
| 489 | while (child != null) { |
| 490 | final StackParentData childParentData = child.parentData! as StackParentData; |
| 491 | if (!childParentData.isPositioned) { |
| 492 | extent = math.max(extent, mainChildSizeGetter(child)); |
| 493 | } |
| 494 | assert(child.parentData == childParentData); |
| 495 | child = childParentData.nextSibling; |
| 496 | } |
| 497 | return extent; |
| 498 | } |
| 499 | |
| 500 | @override |
| 501 | double computeMinIntrinsicWidth(double height) { |
| 502 | return getIntrinsicDimension( |
| 503 | firstChild, |
| 504 | (RenderBox child) => child.getMinIntrinsicWidth(height), |
| 505 | ); |
| 506 | } |
| 507 | |
| 508 | @override |
| 509 | double computeMaxIntrinsicWidth(double height) { |
| 510 | return getIntrinsicDimension( |
| 511 | firstChild, |
| 512 | (RenderBox child) => child.getMaxIntrinsicWidth(height), |
| 513 | ); |
| 514 | } |
| 515 | |
| 516 | @override |
| 517 | double computeMinIntrinsicHeight(double width) { |
| 518 | return getIntrinsicDimension( |
| 519 | firstChild, |
| 520 | (RenderBox child) => child.getMinIntrinsicHeight(width), |
| 521 | ); |
| 522 | } |
| 523 | |
| 524 | @override |
| 525 | double computeMaxIntrinsicHeight(double width) { |
| 526 | return getIntrinsicDimension( |
| 527 | firstChild, |
| 528 | (RenderBox child) => child.getMaxIntrinsicHeight(width), |
| 529 | ); |
| 530 | } |
| 531 | |
| 532 | @override |
| 533 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| 534 | return defaultComputeDistanceToHighestActualBaseline(baseline); |
| 535 | } |
| 536 | |
| 537 | /// Lays out the positioned `child` according to `alignment` within a Stack of `size`. |
| 538 | /// |
| 539 | /// Returns true when the child has visual overflow. |
| 540 | static bool layoutPositionedChild( |
| 541 | RenderBox child, |
| 542 | StackParentData childParentData, |
| 543 | Size size, |
| 544 | Alignment alignment, |
| 545 | ) { |
| 546 | assert(childParentData.isPositioned); |
| 547 | assert(child.parentData == childParentData); |
| 548 | final BoxConstraints childConstraints = childParentData.positionedChildConstraints(size); |
| 549 | child.layout(childConstraints, parentUsesSize: true); |
| 550 | |
| 551 | final double x = switch (childParentData) { |
| 552 | StackParentData(:final double left?) => left, |
| 553 | StackParentData(:final double right?) => size.width - right - child.size.width, |
| 554 | StackParentData() => alignment.alongOffset(size - child.size as Offset).dx, |
| 555 | }; |
| 556 | |
| 557 | final double y = switch (childParentData) { |
| 558 | StackParentData(:final double top?) => top, |
| 559 | StackParentData(:final double bottom?) => size.height - bottom - child.size.height, |
| 560 | StackParentData() => alignment.alongOffset(size - child.size as Offset).dy, |
| 561 | }; |
| 562 | |
| 563 | childParentData.offset = Offset(x, y); |
| 564 | return x < 0.0 || |
| 565 | x + child.size.width > size.width || |
| 566 | y < 0.0 || |
| 567 | y + child.size.height > size.height; |
| 568 | } |
| 569 | |
| 570 | static double? _baselineForChild( |
| 571 | RenderBox child, |
| 572 | Size stackSize, |
| 573 | BoxConstraints nonPositionedChildConstraints, |
| 574 | Alignment alignment, |
| 575 | TextBaseline baseline, |
| 576 | ) { |
| 577 | final StackParentData childParentData = child.parentData! as StackParentData; |
| 578 | final BoxConstraints childConstraints = childParentData.isPositioned |
| 579 | ? childParentData.positionedChildConstraints(stackSize) |
| 580 | : nonPositionedChildConstraints; |
| 581 | final double? baselineOffset = child.getDryBaseline(childConstraints, baseline); |
| 582 | if (baselineOffset == null) { |
| 583 | return null; |
| 584 | } |
| 585 | final double y = switch (childParentData) { |
| 586 | StackParentData(:final double top?) => top, |
| 587 | StackParentData(:final double bottom?) => |
| 588 | stackSize.height - bottom - child.getDryLayout(childConstraints).height, |
| 589 | StackParentData() => |
| 590 | alignment.alongOffset(stackSize - child.getDryLayout(childConstraints) as Offset).dy, |
| 591 | }; |
| 592 | return baselineOffset + y; |
| 593 | } |
| 594 | |
| 595 | @override |
| 596 | double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { |
| 597 | final BoxConstraints nonPositionedChildConstraints = switch (fit) { |
| 598 | StackFit.loose => constraints.loosen(), |
| 599 | StackFit.expand => BoxConstraints.tight(constraints.biggest), |
| 600 | StackFit.passthrough => constraints, |
| 601 | }; |
| 602 | |
| 603 | final Alignment alignment = _resolvedAlignment; |
| 604 | final Size size = getDryLayout(constraints); |
| 605 | |
| 606 | BaselineOffset baselineOffset = BaselineOffset.noBaseline; |
| 607 | for (RenderBox? child = firstChild; child != null; child = childAfter(child)) { |
| 608 | baselineOffset = baselineOffset.minOf( |
| 609 | BaselineOffset( |
| 610 | _baselineForChild(child, size, nonPositionedChildConstraints, alignment, baseline), |
| 611 | ), |
| 612 | ); |
| 613 | } |
| 614 | return baselineOffset.offset; |
| 615 | } |
| 616 | |
| 617 | @override |
| 618 | @protected |
| 619 | Size computeDryLayout(covariant BoxConstraints constraints) { |
| 620 | return _computeSize(constraints: constraints, layoutChild: ChildLayoutHelper.dryLayoutChild); |
| 621 | } |
| 622 | |
| 623 | Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) { |
| 624 | bool hasNonPositionedChildren = false; |
| 625 | if (childCount == 0) { |
| 626 | return constraints.biggest.isFinite ? constraints.biggest : constraints.smallest; |
| 627 | } |
| 628 | |
| 629 | double width = constraints.minWidth; |
| 630 | double height = constraints.minHeight; |
| 631 | |
| 632 | final BoxConstraints nonPositionedConstraints = switch (fit) { |
| 633 | StackFit.loose => constraints.loosen(), |
| 634 | StackFit.expand => BoxConstraints.tight(constraints.biggest), |
| 635 | StackFit.passthrough => constraints, |
| 636 | }; |
| 637 | |
| 638 | RenderBox? child = firstChild; |
| 639 | while (child != null) { |
| 640 | final StackParentData childParentData = child.parentData! as StackParentData; |
| 641 | |
| 642 | if (!childParentData.isPositioned) { |
| 643 | hasNonPositionedChildren = true; |
| 644 | |
| 645 | final Size childSize = layoutChild(child, nonPositionedConstraints); |
| 646 | |
| 647 | width = math.max(width, childSize.width); |
| 648 | height = math.max(height, childSize.height); |
| 649 | } |
| 650 | |
| 651 | child = childParentData.nextSibling; |
| 652 | } |
| 653 | |
| 654 | final Size size; |
| 655 | if (hasNonPositionedChildren) { |
| 656 | size = Size(width, height); |
| 657 | assert(size.width == constraints.constrainWidth(width)); |
| 658 | assert(size.height == constraints.constrainHeight(height)); |
| 659 | } else { |
| 660 | size = constraints.biggest; |
| 661 | } |
| 662 | |
| 663 | assert(size.isFinite); |
| 664 | return size; |
| 665 | } |
| 666 | |
| 667 | @override |
| 668 | void performLayout() { |
| 669 | final BoxConstraints constraints = this.constraints; |
| 670 | _hasVisualOverflow = false; |
| 671 | |
| 672 | size = _computeSize(constraints: constraints, layoutChild: ChildLayoutHelper.layoutChild); |
| 673 | |
| 674 | final Alignment resolvedAlignment = _resolvedAlignment; |
| 675 | RenderBox? child = firstChild; |
| 676 | while (child != null) { |
| 677 | final StackParentData childParentData = child.parentData! as StackParentData; |
| 678 | |
| 679 | if (!childParentData.isPositioned) { |
| 680 | childParentData.offset = resolvedAlignment.alongOffset(size - child.size as Offset); |
| 681 | } else { |
| 682 | _hasVisualOverflow = |
| 683 | layoutPositionedChild(child, childParentData, size, resolvedAlignment) || |
| 684 | _hasVisualOverflow; |
| 685 | } |
| 686 | |
| 687 | assert(child.parentData == childParentData); |
| 688 | child = childParentData.nextSibling; |
| 689 | } |
| 690 | } |
| 691 | |
| 692 | @override |
| 693 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
| 694 | return defaultHitTestChildren(result, position: position); |
| 695 | } |
| 696 | |
| 697 | /// Override in subclasses to customize how the stack paints. |
| 698 | /// |
| 699 | /// By default, the stack uses [defaultPaint]. This function is called by |
| 700 | /// [paint] after potentially applying a clip to contain visual overflow. |
| 701 | @protected |
| 702 | void paintStack(PaintingContext context, Offset offset) { |
| 703 | defaultPaint(context, offset); |
| 704 | } |
| 705 | |
| 706 | @override |
| 707 | void paint(PaintingContext context, Offset offset) { |
| 708 | if (clipBehavior != Clip.none && _hasVisualOverflow) { |
| 709 | _clipRectLayer.layer = context.pushClipRect( |
| 710 | needsCompositing, |
| 711 | offset, |
| 712 | Offset.zero & size, |
| 713 | paintStack, |
| 714 | clipBehavior: clipBehavior, |
| 715 | oldLayer: _clipRectLayer.layer, |
| 716 | ); |
| 717 | } else { |
| 718 | _clipRectLayer.layer = null; |
| 719 | paintStack(context, offset); |
| 720 | } |
| 721 | } |
| 722 | |
| 723 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
| 724 | |
| 725 | @override |
| 726 | void dispose() { |
| 727 | _clipRectLayer.layer = null; |
| 728 | super.dispose(); |
| 729 | } |
| 730 | |
| 731 | @override |
| 732 | Rect? describeApproximatePaintClip(RenderObject child) { |
| 733 | switch (clipBehavior) { |
| 734 | case Clip.none: |
| 735 | return null; |
| 736 | case Clip.hardEdge: |
| 737 | case Clip.antiAlias: |
| 738 | case Clip.antiAliasWithSaveLayer: |
| 739 | return _hasVisualOverflow ? Offset.zero & size : null; |
| 740 | } |
| 741 | } |
| 742 | |
| 743 | @override |
| 744 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 745 | super.debugFillProperties(properties); |
| 746 | properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment' , alignment)); |
| 747 | properties.add(EnumProperty<TextDirection>('textDirection' , textDirection)); |
| 748 | properties.add(EnumProperty<StackFit>('fit' , fit)); |
| 749 | properties.add(EnumProperty<Clip>('clipBehavior' , clipBehavior, defaultValue: Clip.hardEdge)); |
| 750 | } |
| 751 | } |
| 752 | |
| 753 | /// Implements the same layout algorithm as RenderStack but only paints the child |
| 754 | /// specified by index. |
| 755 | /// |
| 756 | /// Although only one child is displayed, the cost of the layout algorithm is |
| 757 | /// still O(N), like an ordinary stack. |
| 758 | class RenderIndexedStack extends RenderStack { |
| 759 | /// Creates a stack render object that paints a single child. |
| 760 | /// |
| 761 | /// If the [index] parameter is null, nothing is displayed. |
| 762 | RenderIndexedStack({ |
| 763 | super.children, |
| 764 | super.alignment, |
| 765 | super.textDirection, |
| 766 | super.fit, |
| 767 | super.clipBehavior, |
| 768 | int? index = 0, |
| 769 | }) : _index = index; |
| 770 | |
| 771 | @override |
| 772 | void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
| 773 | final RenderBox? displayedChild = _childAtIndex(); |
| 774 | if (displayedChild != null) { |
| 775 | visitor(displayedChild); |
| 776 | } |
| 777 | } |
| 778 | |
| 779 | /// The index of the child to show, null if nothing is to be displayed. |
| 780 | int? get index => _index; |
| 781 | int? _index; |
| 782 | set index(int? value) { |
| 783 | if (_index != value) { |
| 784 | _index = value; |
| 785 | markNeedsLayout(); |
| 786 | } |
| 787 | } |
| 788 | |
| 789 | RenderBox? _childAtIndex() { |
| 790 | final int? index = this.index; |
| 791 | if (index == null) { |
| 792 | return null; |
| 793 | } |
| 794 | RenderBox? child = firstChild; |
| 795 | for (int i = 0; i < index && child != null; i += 1) { |
| 796 | child = childAfter(child); |
| 797 | } |
| 798 | assert(firstChild == null || child != null); |
| 799 | return child; |
| 800 | } |
| 801 | |
| 802 | @override |
| 803 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| 804 | final RenderBox? displayedChild = _childAtIndex(); |
| 805 | if (displayedChild == null) { |
| 806 | return null; |
| 807 | } |
| 808 | final StackParentData childParentData = displayedChild.parentData! as StackParentData; |
| 809 | final BaselineOffset offset = |
| 810 | BaselineOffset(displayedChild.getDistanceToActualBaseline(baseline)) + |
| 811 | childParentData.offset.dy; |
| 812 | return offset.offset; |
| 813 | } |
| 814 | |
| 815 | @override |
| 816 | double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) { |
| 817 | final RenderBox? displayedChild = _childAtIndex(); |
| 818 | if (displayedChild == null) { |
| 819 | return null; |
| 820 | } |
| 821 | final BoxConstraints nonPositionedChildConstraints = switch (fit) { |
| 822 | StackFit.loose => constraints.loosen(), |
| 823 | StackFit.expand => BoxConstraints.tight(constraints.biggest), |
| 824 | StackFit.passthrough => constraints, |
| 825 | }; |
| 826 | |
| 827 | final Alignment alignment = _resolvedAlignment; |
| 828 | final Size size = getDryLayout(constraints); |
| 829 | |
| 830 | return RenderStack._baselineForChild( |
| 831 | displayedChild, |
| 832 | size, |
| 833 | nonPositionedChildConstraints, |
| 834 | alignment, |
| 835 | baseline, |
| 836 | ); |
| 837 | } |
| 838 | |
| 839 | @override |
| 840 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
| 841 | final RenderBox? displayedChild = _childAtIndex(); |
| 842 | if (displayedChild == null) { |
| 843 | return false; |
| 844 | } |
| 845 | final StackParentData childParentData = displayedChild.parentData! as StackParentData; |
| 846 | return result.addWithPaintOffset( |
| 847 | offset: childParentData.offset, |
| 848 | position: position, |
| 849 | hitTest: (BoxHitTestResult result, Offset transformed) { |
| 850 | assert(transformed == position - childParentData.offset); |
| 851 | return displayedChild.hitTest(result, position: transformed); |
| 852 | }, |
| 853 | ); |
| 854 | } |
| 855 | |
| 856 | @override |
| 857 | void paintStack(PaintingContext context, Offset offset) { |
| 858 | final RenderBox? displayedChild = _childAtIndex(); |
| 859 | if (displayedChild == null) { |
| 860 | return; |
| 861 | } |
| 862 | final StackParentData childParentData = displayedChild.parentData! as StackParentData; |
| 863 | context.paintChild(displayedChild, childParentData.offset + offset); |
| 864 | } |
| 865 | |
| 866 | @override |
| 867 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 868 | super.debugFillProperties(properties); |
| 869 | properties.add(IntProperty('index' , index)); |
| 870 | } |
| 871 | |
| 872 | @override |
| 873 | List<DiagnosticsNode> debugDescribeChildren() { |
| 874 | final List<DiagnosticsNode> children = <DiagnosticsNode>[]; |
| 875 | int i = 0; |
| 876 | RenderObject? child = firstChild; |
| 877 | while (child != null) { |
| 878 | children.add( |
| 879 | child.toDiagnosticsNode( |
| 880 | name: 'child ${i + 1}' , |
| 881 | style: i != index ? DiagnosticsTreeStyle.offstage : null, |
| 882 | ), |
| 883 | ); |
| 884 | child = (child.parentData! as StackParentData).nextSibling; |
| 885 | i += 1; |
| 886 | } |
| 887 | return children; |
| 888 | } |
| 889 | } |
| 890 | |