• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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