stack.dart 18 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;
6
import 'dart:ui' show lerpDouble, hashValues;
7

8
import 'package:flutter/foundation.dart';
9

10 11
import 'box.dart';
import 'object.dart';
12

Hixie's avatar
Hixie committed
13 14 15 16 17 18
/// An immutable 2D, axis-aligned, floating-point rectangle whose coordinates
/// are given relative to another rectangle's edges, known as the container.
/// Since the dimensions of the rectangle are relative to those of the
/// container, this class has no width and height members. To determine the
/// width or height of the rectangle, convert it to a [Rect] using [toRect()]
/// (passing the container's own Rect), and then examine that object.
Hixie's avatar
Hixie committed
19 20 21
///
/// If you create the RelativeRect with null values, the methods on
/// RelativeRect will not work usefully (or at all).
Hixie's avatar
Hixie committed
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
class RelativeRect {
  /// Creates a RelativeRect with the given values.
  const RelativeRect.fromLTRB(this.left, this.top, this.right, this.bottom);

  /// Creates a RelativeRect from a Rect and a Size. The Rect (first argument)
  /// and the RelativeRect (the output) are in the coordinate space of the
  /// rectangle described by the Size, with 0,0 being at the top left.
  factory RelativeRect.fromSize(Rect rect, Size container) {
    return new RelativeRect.fromLTRB(rect.left, rect.top, container.width - rect.right, container.height - rect.bottom);
  }

  /// Creates a RelativeRect from two Rects. The second Rect provides the
  /// container, the first provides the rectangle, in the same coordinate space,
  /// that is to be converted to a RelativeRect. The output will be in the
  /// container's coordinate space.
  ///
  /// For example, if the top left of the rect is at 0,0, and the top left of
  /// the container is at 100,100, then the top left of the output will be at
  /// -100,-100.
  ///
  /// If the first rect is actually in the container's coordinate space, then
  /// use [RelativeRect.fromSize] and pass the container's size as the second
  /// argument instead.
  factory RelativeRect.fromRect(Rect rect, Rect container) {
    return new RelativeRect.fromLTRB(
      rect.left - container.left,
      rect.top - container.top,
49 50
      container.right - rect.right,
      container.bottom - rect.bottom
Hixie's avatar
Hixie committed
51 52 53
    );
  }

54
  /// A rect that covers the entire container.
55
  static final RelativeRect fill = const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0);
Hixie's avatar
Hixie committed
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

  /// Distance from the left side of the container to the left side of this rectangle.
  final double left;

  /// Distance from the top side of the container to the top side of this rectangle.
  final double top;

  /// Distance from the right side of the container to the right side of this rectangle.
  final double right;

  /// Distance from the bottom side of the container to the bottom side of this rectangle.
  final double bottom;

  /// Returns a new rectangle object translated by the given offset.
  RelativeRect shift(Offset offset) {
71
    return new RelativeRect.fromLTRB(left + offset.dx, top + offset.dy, right - offset.dx, bottom - offset.dy);
Hixie's avatar
Hixie committed
72 73 74 75
  }

  /// Returns a new rectangle with edges moved outwards by the given delta.
  RelativeRect inflate(double delta) {
76
    return new RelativeRect.fromLTRB(left - delta, top - delta, right - delta, bottom - delta);
Hixie's avatar
Hixie committed
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  }

  /// Returns a new rectangle with edges moved inwards by the given delta.
  RelativeRect deflate(double delta) {
    return inflate(-delta);
  }

  /// Returns a new rectangle that is the intersection of the given rectangle and this rectangle.
  RelativeRect intersect(RelativeRect other) {
    return new RelativeRect.fromLTRB(
      math.max(left, other.left),
      math.max(top, other.top),
      math.max(right, other.right),
      math.max(bottom, other.bottom)
    );
  }

94
  /// Convert this [RelativeRect] to a [Rect], in the coordinate space of the container.
Hixie's avatar
Hixie committed
95 96 97 98 99 100 101 102 103 104 105 106 107
  Rect toRect(Rect container) {
    return new Rect.fromLTRB(left, top, container.width - right, container.height - bottom);
  }

  /// Linearly interpolate between two RelativeRects.
  ///
  /// If either rect is null, this function interpolates from [RelativeRect.fill].
  static RelativeRect lerp(RelativeRect a, RelativeRect b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return new RelativeRect.fromLTRB(b.left * t, b.top * t, b.right * t, b.bottom * t);
    if (b == null) {
108
      final double k = 1.0 - t;
Hixie's avatar
Hixie committed
109 110 111 112 113 114 115 116 117 118
      return new RelativeRect.fromLTRB(b.left * k, b.top * k, b.right * k, b.bottom * k);
    }
    return new RelativeRect.fromLTRB(
      lerpDouble(a.left, b.left, t),
      lerpDouble(a.top, b.top, t),
      lerpDouble(a.right, b.right, t),
      lerpDouble(a.bottom, b.bottom, t)
    );
  }

119
  @override
Hixie's avatar
Hixie committed
120 121 122 123 124 125 126 127 128 129 130 131
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! RelativeRect)
      return false;
    final RelativeRect typedOther = other;
    return left == typedOther.left &&
           top == typedOther.top &&
           right == typedOther.right &&
           bottom == typedOther.bottom;
  }

132
  @override
133
  int get hashCode => hashValues(left, top, right, bottom);
Hixie's avatar
Hixie committed
134

135
  @override
Hixie's avatar
Hixie committed
136
  String toString() => "RelativeRect.fromLTRB(${left?.toStringAsFixed(1)}, ${top?.toStringAsFixed(1)}, ${right?.toStringAsFixed(1)}, ${bottom?.toStringAsFixed(1)})";
Hixie's avatar
Hixie committed
137 138
}

Adam Barth's avatar
Adam Barth committed
139
/// Parent data for use with [RenderStack].
Hixie's avatar
Hixie committed
140
class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
Hixie's avatar
Hixie committed
141
  /// The distance by which the child's top edge is inset from the top of the stack.
142
  double top;
143

Hixie's avatar
Hixie committed
144
  /// The distance by which the child's right edge is inset from the right of the stack.
145
  double right;
146

Hixie's avatar
Hixie committed
147
  /// The distance by which the child's bottom edge is inset from the bottom of the stack.
148
  double bottom;
149

Hixie's avatar
Hixie committed
150
  /// The distance by which the child's left edge is inset from the left of the stack.
151 152
  double left;

153 154 155 156 157 158 159 160 161 162
  /// The child's width.
  ///
  /// Ignored if both left and right are non-null.
  double width;

  /// The child's height.
  ///
  /// Ignored if both top and bottom are non-null.
  double height;

Hixie's avatar
Hixie committed
163 164
  /// Get or set the current values in terms of a RelativeRect object.
  RelativeRect get rect => new RelativeRect.fromLTRB(left, top, right, bottom);
165
  set rect(RelativeRect value) {
Hixie's avatar
Hixie committed
166 167 168
    top = value.top;
    right = value.right;
    bottom = value.bottom;
Hixie's avatar
Hixie committed
169
    left = value.left;
Hixie's avatar
Hixie committed
170 171
  }

Hixie's avatar
Hixie committed
172
  /// Whether this child is considered positioned.
173
  ///
Hixie's avatar
Hixie committed
174
  /// A child is positioned if any of the top, right, bottom, or left properties
175 176 177
  /// are non-null. Positioned children do not factor into determining the size
  /// of the stack but are instead placed relative to the non-positioned
  /// children in the stack.
178
  bool get isPositioned => top != null || right != null || bottom != null || left != null || width != null || height != null;
179

180
  @override
Hixie's avatar
Hixie committed
181
  String toString() {
182
    final List<String> values = <String>[];
Hixie's avatar
Hixie committed
183 184 185 186 187 188 189 190 191 192 193 194
    if (top != null)
      values.add('top=$top');
    if (right != null)
      values.add('right=$right');
    if (bottom != null)
      values.add('bottom=$bottom');
    if (left != null)
      values.add('left=$left');
    if (width != null)
      values.add('width=$width');
    if (height != null)
      values.add('height=$height');
195 196 197
    if (values.isEmpty)
      values.add('not positioned');
    values.add(super.toString());
Hixie's avatar
Hixie committed
198 199
    return values.join('; ');
  }
200 201
}

202
/// Whether overflowing children should be clipped, or their overflow be
203 204
/// visible.
enum Overflow {
205
  /// Overflowing children will be visible.
206
  visible,
207 208
  /// Overflowing children will be clipped to the bounds of their parent.
  clip,
209 210
}

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
/// Implements the stack layout algorithm
///
/// In a stack layout, the children are positioned on top of each other in the
/// order in which they appear in the child list. First, the non-positioned
/// children (those with null values for top, right, bottom, and left) are
/// laid out and initially placed in the upper-left corner of the stack. The
/// stack is then sized to enclose all of the non-positioned children. If there
/// are no non-positioned children, the stack becomes as large as possible.
///
/// The final location of non-positioned children is determined by the alignment
/// parameter. The left of each non-positioned child becomes the
/// difference between the child's width and the stack's width scaled by
/// alignment.x. The top of each non-positioned child is computed
/// similarly and scaled by alignment.y. So if the alignment x and y properties
/// are 0.0 (the default) then the non-positioned children remain in the
/// upper-left corner. If the alignment x and y properties are 0.5 then the
/// non-positioned children are centered within the stack.
///
/// Next, the positioned children are laid out. If a child has top and bottom
/// values that are both non-null, the child is given a fixed height determined
/// by subtracting the sum of the top and bottom values from the height of the stack.
/// Similarly, if the child has right and left values that are both non-null,
/// the child is given a fixed width derived from the stack's width.
/// Otherwise, the child is given unbounded constraints in the non-fixed dimensions.
///
/// Once the child is laid out, the stack positions the child
/// according to the top, right, bottom, and left properties of their
/// [StackParentData]. For example, if the bottom value is 10.0, the
/// bottom edge of the child will be inset 10.0 pixels from the bottom
/// edge of the stack. If the child extends beyond the bounds of the
/// stack, the stack will clip the child's painting to the bounds of
/// the stack.
///
/// See also:
///
///  * [RenderFlow]
class RenderStack extends RenderBox
Hans Muller's avatar
Hans Muller committed
248 249
    with ContainerRenderObjectMixin<RenderBox, StackParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
250 251 252 253 254
  /// Creates a stack render object.
  ///
  /// By default, the non-positioned children of the stack are aligned by their
  /// top left corners.
  RenderStack({
Hans Muller's avatar
Hans Muller committed
255
    List<RenderBox> children,
256 257 258 259 260
    FractionalOffset alignment: FractionalOffset.topLeft,
    Overflow overflow: Overflow.clip
  }) : _alignment = alignment,
       _overflow = overflow {
    assert(overflow != null);
261 262 263
    addAll(children);
  }

264 265
  bool _hasVisualOverflow = false;

266
  @override
267 268 269 270 271
  void setupParentData(RenderBox child) {
    if (child.parentData is! StackParentData)
      child.parentData = new StackParentData();
  }

272 273 274 275 276 277 278 279 280 281 282 283 284 285
  /// Whether overflowing children should be clipped. See [Overflow].
  ///
  /// Some children in a stack might overflow its box. When this flag is set to
  /// [Overflow.clipped], children cannot paint outside of the stack's box.
  Overflow get overflow => _overflow;
  Overflow _overflow;
  set overflow (Overflow value) {
    assert(value != null);
    if (_overflow != value) {
      _overflow = value;
      markNeedsPaint();
    }
  }

286 287 288 289 290 291
  /// How to align the non-positioned children in the stack.
  ///
  /// The non-positioned children are placed relative to each other such that
  /// the points determined by [alignment] are co-located. For example, if the
  /// [alignment] is [FractionalOffset.topLeft], then the top left corner of
  /// each non-positioned child will be located at the same global coordinate.
292 293
  FractionalOffset get alignment => _alignment;
  FractionalOffset _alignment;
294
  set alignment (FractionalOffset value) {
295 296
    if (_alignment != value) {
      _alignment = value;
Hans Muller's avatar
Hans Muller committed
297 298 299 300
      markNeedsLayout();
    }
  }

301 302
  double _getIntrinsicDimension(double mainChildSizeGetter(RenderBox child)) {
    double extent = 0.0;
303 304
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
305 306
      final StackParentData childParentData = child.parentData;
      if (!childParentData.isPositioned)
307
        extent = math.max(extent, mainChildSizeGetter(child));
Hixie's avatar
Hixie committed
308 309
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
310
    }
311
    return extent;
312 313
  }

314
  @override
315
  double computeMinIntrinsicWidth(double height) {
316
    return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicWidth(height));
317 318
  }

319
  @override
320
  double computeMaxIntrinsicWidth(double height) {
321
    return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicWidth(height));
322 323
  }

324
  @override
325
  double computeMinIntrinsicHeight(double width) {
326 327 328 329
    return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicHeight(width));
  }

  @override
330
  double computeMaxIntrinsicHeight(double width) {
331
    return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicHeight(width));
332 333
  }

334
  @override
335 336 337 338
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

339
  @override
340
  void performLayout() {
341
    _hasVisualOverflow = false;
342 343 344 345 346 347 348
    bool hasNonPositionedChildren = false;

    double width = 0.0;
    double height = 0.0;

    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
349
      final StackParentData childParentData = child.parentData;
350

Hixie's avatar
Hixie committed
351
      if (!childParentData.isPositioned) {
352 353 354
        hasNonPositionedChildren = true;

        child.layout(constraints, parentUsesSize: true);
355
        childParentData.offset = Offset.zero;
356 357 358 359 360 361

        final Size childSize = child.size;
        width = math.max(width, childSize.width);
        height = math.max(height, childSize.height);
      }

Hixie's avatar
Hixie committed
362
      child = childParentData.nextSibling;
363 364
    }

365
    if (hasNonPositionedChildren) {
366
      size = new Size(width, height);
367 368 369
      assert(size.width == constraints.constrainWidth(width));
      assert(size.height == constraints.constrainHeight(height));
    } else {
370
      size = constraints.biggest;
371
    }
372 373 374 375 376

    assert(!size.isInfinite);

    child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
377
      final StackParentData childParentData = child.parentData;
378

Hixie's avatar
Hixie committed
379
      if (!childParentData.isPositioned) {
380
        childParentData.offset = alignment.alongOffset(size - child.size);
Hans Muller's avatar
Hans Muller committed
381
      } else {
382
        BoxConstraints childConstraints = const BoxConstraints();
383

Hixie's avatar
Hixie committed
384
        if (childParentData.left != null && childParentData.right != null)
385
          childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
386
        else if (childParentData.width != null)
387
          childConstraints = childConstraints.tighten(width: childParentData.width);
388

Hixie's avatar
Hixie committed
389
        if (childParentData.top != null && childParentData.bottom != null)
390
          childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
391
        else if (childParentData.height != null)
392
          childConstraints = childConstraints.tighten(height: childParentData.height);
393 394 395 396

        child.layout(childConstraints, parentUsesSize: true);

        double x = 0.0;
Hixie's avatar
Hixie committed
397 398 399 400
        if (childParentData.left != null)
          x = childParentData.left;
        else if (childParentData.right != null)
          x = size.width - childParentData.right - child.size.width;
401 402 403

        if (x < 0.0 || x + child.size.width > size.width)
          _hasVisualOverflow = true;
404 405

        double y = 0.0;
Hixie's avatar
Hixie committed
406 407 408 409
        if (childParentData.top != null)
          y = childParentData.top;
        else if (childParentData.bottom != null)
          y = size.height - childParentData.bottom - child.size.height;
410 411 412

        if (y < 0.0 || y + child.size.height > size.height)
          _hasVisualOverflow = true;
413

414
        childParentData.offset = new Offset(x, y);
415 416
      }

Hixie's avatar
Hixie committed
417 418
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
419 420 421
    }
  }

422
  @override
Adam Barth's avatar
Adam Barth committed
423 424
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
425 426
  }

427 428 429 430 431 432 433 434
  /// Override in subclasses to customize how the stack paints.
  ///
  /// By default, the stack uses [defaultPaint]. This function is called by
  /// [paint] after potentially applying a clip to contain visual overflow.
  @protected
  void paintStack(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }
Hans Muller's avatar
Hans Muller committed
435

436
  @override
437
  void paint(PaintingContext context, Offset offset) {
438
    if (_overflow == Overflow.clip && _hasVisualOverflow) {
439
      context.pushClipRect(needsCompositing, offset, Point.origin & size, paintStack);
440
    } else {
Hans Muller's avatar
Hans Muller committed
441
      paintStack(context, offset);
442
    }
443
  }
Hixie's avatar
Hixie committed
444

445
  @override
Hixie's avatar
Hixie committed
446
  Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
447
}
Hans Muller's avatar
Hans Muller committed
448 449 450

/// Implements the same layout algorithm as RenderStack but only paints the child
/// specified by index.
451 452
///
/// Although only one child is displayed, the cost of the layout algorithm is
Hans Muller's avatar
Hans Muller committed
453
/// still O(N), like an ordinary stack.
454 455 456
class RenderIndexedStack extends RenderStack {
  /// Creates a stack render object that paints a single child.
  ///
457
  /// If the [index] parameter is null, nothing is displayed.
Hans Muller's avatar
Hans Muller committed
458 459
  RenderIndexedStack({
    List<RenderBox> children,
460
    FractionalOffset alignment: FractionalOffset.topLeft,
Hans Muller's avatar
Hans Muller committed
461 462 463
    int index: 0
  }) : _index = index, super(
   children: children,
464
   alignment: alignment
465
  );
Hans Muller's avatar
Hans Muller committed
466

467
  /// The index of the child to show, null if nothing is to be displayed.
Hans Muller's avatar
Hans Muller committed
468 469
  int get index => _index;
  int _index;
470
  set index (int value) {
Hans Muller's avatar
Hans Muller committed
471 472 473 474 475 476 477
    if (_index != value) {
      _index = value;
      markNeedsLayout();
    }
  }

  RenderBox _childAtIndex() {
478
    assert(index != null);
Hans Muller's avatar
Hans Muller committed
479 480 481
    RenderBox child = firstChild;
    int i = 0;
    while (child != null && i < index) {
Hixie's avatar
Hixie committed
482 483
      final StackParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
Hans Muller's avatar
Hans Muller committed
484 485 486 487 488 489 490
      i += 1;
    }
    assert(i == index);
    assert(child != null);
    return child;
  }

491
  @override
Adam Barth's avatar
Adam Barth committed
492
  bool hitTestChildren(HitTestResult result, { Point position }) {
493
    if (firstChild == null || index == null)
Adam Barth's avatar
Adam Barth committed
494
      return false;
Hans Muller's avatar
Hans Muller committed
495
    assert(position != null);
496
    final RenderBox child = _childAtIndex();
Hixie's avatar
Hixie committed
497
    final StackParentData childParentData = child.parentData;
498
    final Point transformed = new Point(position.x - childParentData.offset.dx,
499
                                  position.y - childParentData.offset.dy);
Adam Barth's avatar
Adam Barth committed
500
    return child.hitTest(result, position: transformed);
Hans Muller's avatar
Hans Muller committed
501 502
  }

503
  @override
Hans Muller's avatar
Hans Muller committed
504
  void paintStack(PaintingContext context, Offset offset) {
505
    if (firstChild == null || index == null)
Hans Muller's avatar
Hans Muller committed
506
      return;
507
    final RenderBox child = _childAtIndex();
Hixie's avatar
Hixie committed
508
    final StackParentData childParentData = child.parentData;
Adam Barth's avatar
Adam Barth committed
509
    context.paintChild(child, childParentData.offset + offset);
Hans Muller's avatar
Hans Muller committed
510 511
  }
}