stack.dart 17.1 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 9
import 'package:meta/meta.dart';

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.
Hixie's avatar
Hixie committed
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  static final RelativeRect fill = new RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0);

  /// 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) {
    return new RelativeRect.fromLTRB(left + offset.dx, top + offset.dy, right + offset.dx, bottom + offset.dy);
  }

  /// Returns a new rectangle with edges moved outwards by the given delta.
  RelativeRect inflate(double delta) {
    return new RelativeRect.fromLTRB(left - delta, top - delta, right + delta, bottom + delta);
  }

  /// 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)
    );
  }

  /// Convert this RelativeRect to a Rect, in the coordinate space of the container.
  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) {
      double k = 1.0 - t;
      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 182 183 184 185 186 187 188 189 190 191 192 193 194
  String toString() {
    List<String> values = <String>[];
    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 203 204 205 206 207 208 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
/// 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
239 240
    with ContainerRenderObjectMixin<RenderBox, StackParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
241 242 243 244 245
  /// 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
246
    List<RenderBox> children,
247
    FractionalOffset alignment: FractionalOffset.topLeft
248
  }) : _alignment = alignment {
249 250 251
    addAll(children);
  }

252 253
  bool _hasVisualOverflow = false;

254
  @override
255 256 257 258 259
  void setupParentData(RenderBox child) {
    if (child.parentData is! StackParentData)
      child.parentData = new StackParentData();
  }

260 261 262 263 264 265
  /// 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.
266 267
  FractionalOffset get alignment => _alignment;
  FractionalOffset _alignment;
268
  set alignment (FractionalOffset value) {
269 270
    if (_alignment != value) {
      _alignment = value;
Hans Muller's avatar
Hans Muller committed
271 272 273 274
      markNeedsLayout();
    }
  }

275 276
  double _getIntrinsicDimension(double mainChildSizeGetter(RenderBox child)) {
    double extent = 0.0;
277 278
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
279 280
      final StackParentData childParentData = child.parentData;
      if (!childParentData.isPositioned)
281
        extent = math.max(extent, mainChildSizeGetter(child));
Hixie's avatar
Hixie committed
282 283
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
284
    }
285
    return extent;
286 287
  }

288
  @override
289
  double computeMinIntrinsicWidth(double height) {
290
    return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicWidth(height));
291 292
  }

293
  @override
294
  double computeMaxIntrinsicWidth(double height) {
295
    return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicWidth(height));
296 297
  }

298
  @override
299
  double computeMinIntrinsicHeight(double width) {
300 301 302 303
    return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicHeight(width));
  }

  @override
304
  double computeMaxIntrinsicHeight(double width) {
305
    return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicHeight(width));
306 307
  }

308
  @override
309 310 311 312
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

313
  @override
314
  void performLayout() {
315
    _hasVisualOverflow = false;
316 317 318 319 320 321 322
    bool hasNonPositionedChildren = false;

    double width = 0.0;
    double height = 0.0;

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

Hixie's avatar
Hixie committed
325
      if (!childParentData.isPositioned) {
326 327 328
        hasNonPositionedChildren = true;

        child.layout(constraints, parentUsesSize: true);
329
        childParentData.offset = Offset.zero;
330 331 332 333 334 335

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

Hixie's avatar
Hixie committed
336
      child = childParentData.nextSibling;
337 338
    }

339
    if (hasNonPositionedChildren) {
340
      size = new Size(width, height);
341 342 343
      assert(size.width == constraints.constrainWidth(width));
      assert(size.height == constraints.constrainHeight(height));
    } else {
344
      size = constraints.biggest;
345
    }
346 347 348 349 350

    assert(!size.isInfinite);

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

Hixie's avatar
Hixie committed
353
      if (!childParentData.isPositioned) {
354
        childParentData.offset = alignment.alongOffset(size - child.size);
Hans Muller's avatar
Hans Muller committed
355
      } else {
356
        BoxConstraints childConstraints = const BoxConstraints();
357

Hixie's avatar
Hixie committed
358
        if (childParentData.left != null && childParentData.right != null)
359
          childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
360
        else if (childParentData.width != null)
361
          childConstraints = childConstraints.tighten(width: childParentData.width);
362

Hixie's avatar
Hixie committed
363
        if (childParentData.top != null && childParentData.bottom != null)
364
          childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
365
        else if (childParentData.height != null)
366
          childConstraints = childConstraints.tighten(height: childParentData.height);
367 368 369 370

        child.layout(childConstraints, parentUsesSize: true);

        double x = 0.0;
Hixie's avatar
Hixie committed
371 372 373 374
        if (childParentData.left != null)
          x = childParentData.left;
        else if (childParentData.right != null)
          x = size.width - childParentData.right - child.size.width;
375 376 377

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

        double y = 0.0;
Hixie's avatar
Hixie committed
380 381 382 383
        if (childParentData.top != null)
          y = childParentData.top;
        else if (childParentData.bottom != null)
          y = size.height - childParentData.bottom - child.size.height;
384 385 386

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

388
        childParentData.offset = new Offset(x, y);
389 390
      }

Hixie's avatar
Hixie committed
391 392
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
393 394 395
    }
  }

396
  @override
Adam Barth's avatar
Adam Barth committed
397 398
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
399 400
  }

401 402 403 404 405 406 407 408
  /// 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
409

410
  @override
411
  void paint(PaintingContext context, Offset offset) {
412
    if (_hasVisualOverflow) {
413
      context.pushClipRect(needsCompositing, offset, Point.origin & size, paintStack);
414
    } else {
Hans Muller's avatar
Hans Muller committed
415
      paintStack(context, offset);
416
    }
417
  }
Hixie's avatar
Hixie committed
418

419
  @override
Hixie's avatar
Hixie committed
420
  Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
421
}
Hans Muller's avatar
Hans Muller committed
422 423 424

/// Implements the same layout algorithm as RenderStack but only paints the child
/// specified by index.
425 426
///
/// Although only one child is displayed, the cost of the layout algorithm is
Hans Muller's avatar
Hans Muller committed
427
/// still O(N), like an ordinary stack.
428 429 430 431
class RenderIndexedStack extends RenderStack {
  /// Creates a stack render object that paints a single child.
  ///
  /// The [index] argument must not be null.
Hans Muller's avatar
Hans Muller committed
432 433
  RenderIndexedStack({
    List<RenderBox> children,
434
    FractionalOffset alignment: FractionalOffset.topLeft,
Hans Muller's avatar
Hans Muller committed
435 436 437
    int index: 0
  }) : _index = index, super(
   children: children,
438
   alignment: alignment
439 440 441
  ) {
    assert(index != null);
  }
Hans Muller's avatar
Hans Muller committed
442

443
  /// The index of the child to show.
Hans Muller's avatar
Hans Muller committed
444 445
  int get index => _index;
  int _index;
446
  set index (int value) {
447
    assert(value != null);
Hans Muller's avatar
Hans Muller committed
448 449 450 451 452 453 454 455 456 457
    if (_index != value) {
      _index = value;
      markNeedsLayout();
    }
  }

  RenderBox _childAtIndex() {
    RenderBox child = firstChild;
    int i = 0;
    while (child != null && i < index) {
Hixie's avatar
Hixie committed
458 459
      final StackParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
Hans Muller's avatar
Hans Muller committed
460 461 462 463 464 465 466
      i += 1;
    }
    assert(i == index);
    assert(child != null);
    return child;
  }

467
  @override
Adam Barth's avatar
Adam Barth committed
468
  bool hitTestChildren(HitTestResult result, { Point position }) {
Hans Muller's avatar
Hans Muller committed
469
    if (firstChild == null)
Adam Barth's avatar
Adam Barth committed
470
      return false;
Hans Muller's avatar
Hans Muller committed
471 472
    assert(position != null);
    RenderBox child = _childAtIndex();
Hixie's avatar
Hixie committed
473
    final StackParentData childParentData = child.parentData;
474 475
    Point transformed = new Point(position.x - childParentData.offset.dx,
                                  position.y - childParentData.offset.dy);
Adam Barth's avatar
Adam Barth committed
476
    return child.hitTest(result, position: transformed);
Hans Muller's avatar
Hans Muller committed
477 478
  }

479
  @override
Hans Muller's avatar
Hans Muller committed
480 481 482 483
  void paintStack(PaintingContext context, Offset offset) {
    if (firstChild == null)
      return;
    RenderBox child = _childAtIndex();
Hixie's avatar
Hixie committed
484
    final StackParentData childParentData = child.parentData;
Adam Barth's avatar
Adam Barth committed
485
    context.paintChild(child, childParentData.offset + offset);
Hans Muller's avatar
Hans Muller committed
486 487
  }
}