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