stack.dart 17.4 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;
Hixie's avatar
Hixie committed
6
import 'dart:ui' show lerpDouble;
7

8 9
import 'box.dart';
import 'object.dart';
10

Hixie's avatar
Hixie committed
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
/// 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.
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,
45 46
      container.right - rect.right,
      container.bottom - rect.bottom
Hixie's avatar
Hixie committed
47 48 49 50 51 52 53 54 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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    );
  }

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

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

  int get hashCode {
    int value = 373;
    value = 37 * value + left.hashCode;
    value = 37 * value + top.hashCode;
    value = 37 * value + right.hashCode;
    value = 37 * value + bottom.hashCode;
    return value;
  }

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

138
/// Parent data for use with [RenderStack]
Hixie's avatar
Hixie committed
139
class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
140
  /// The offset of the child's top edge from the top of the stack.
141
  double top;
142

143
  /// The offset of the child's right edge from the right of the stack.
144
  double right;
145

146
  /// The offset of the child's bottom edge from the bottom of the stack.
147
  double bottom;
148

149
  /// The offset of the child's left edge from the left of the stack.
150 151
  double left;

152 153 154 155 156 157 158 159 160 161
  /// 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
162 163 164 165 166 167 168 169 170
  /// Get or set the current values in terms of a RelativeRect object.
  RelativeRect get rect => new RelativeRect.fromLTRB(left, top, right, bottom);
  void set rect(RelativeRect value) {
    left = value.left;
    top = value.top;
    right = value.right;
    bottom = value.bottom;
  }

171 172 173 174 175 176 177 178 179
  void merge(StackParentData other) {
    if (other.top != null)
      top = other.top;
    if (other.right != null)
      right = other.right;
    if (other.bottom != null)
      bottom = other.bottom;
    if (other.left != null)
      left = other.left;
180 181 182 183
    if (other.width != null)
      width = other.width;
    if (other.height != null)
      height = other.height;
184 185 186
    super.merge(other);
  }

187 188 189 190 191 192
  /// Whether this child is considered positioned
  ///
  /// A child is positioned if any of the top, right, bottom, or left offsets
  /// 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.
193
  bool get isPositioned => top != null || right != null || bottom != null || left != null || width != null || height != null;
194

Adam Barth's avatar
Adam Barth committed
195
  String toString() => '${super.toString()}; top=$top; right=$right; bottom=$bottom; left=$left; width=$width; height=$height';
196 197
}

Hans Muller's avatar
Hans Muller committed
198 199 200 201 202
abstract class RenderStackBase extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, StackParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
  RenderStackBase({
    List<RenderBox> children,
203 204
    alignment: const FractionalOffset(0.0, 0.0)
  }) : _alignment = alignment {
205 206 207
    addAll(children);
  }

208 209
  bool _hasVisualOverflow = false;

210 211 212 213 214
  void setupParentData(RenderBox child) {
    if (child.parentData is! StackParentData)
      child.parentData = new StackParentData();
  }

215 216 217 218 219
  FractionalOffset get alignment => _alignment;
  FractionalOffset _alignment;
  void set alignment (FractionalOffset value) {
    if (_alignment != value) {
      _alignment = value;
Hans Muller's avatar
Hans Muller committed
220 221 222 223
      markNeedsLayout();
    }
  }

224 225 226 227
  double getMinIntrinsicWidth(BoxConstraints constraints) {
    double width = constraints.minWidth;
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
228 229
      final StackParentData childParentData = child.parentData;
      if (!childParentData.isPositioned)
230
        width = math.max(width, child.getMinIntrinsicWidth(constraints));
Hixie's avatar
Hixie committed
231 232
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
233 234 235 236 237 238 239 240 241 242
    }
    assert(width == constraints.constrainWidth(width));
    return width;
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
    bool hasNonPositionedChildren = false;
    double width = constraints.minWidth;
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
243 244
      final StackParentData childParentData = child.parentData;
      if (!childParentData.isPositioned) {
245 246 247
        hasNonPositionedChildren = true;
        width = math.max(width, child.getMaxIntrinsicWidth(constraints));
      }
Hixie's avatar
Hixie committed
248 249
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
250 251 252 253 254 255 256 257 258 259 260
    }
    if (!hasNonPositionedChildren)
      return constraints.constrainWidth();
    assert(width == constraints.constrainWidth(width));
    return width;
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
    double height = constraints.minHeight;
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
261 262
      final StackParentData childParentData = child.parentData;
      if (!childParentData.isPositioned)
263
        height = math.max(height, child.getMinIntrinsicHeight(constraints));
Hixie's avatar
Hixie committed
264 265
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
266 267 268 269 270 271 272 273 274 275
    }
    assert(height == constraints.constrainHeight(height));
    return height;
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
    bool hasNonPositionedChildren = false;
    double height = constraints.minHeight;
    RenderBox child = firstChild;
    while (child != null) {
Hixie's avatar
Hixie committed
276 277
      final StackParentData childParentData = child.parentData;
      if (!childParentData.isPositioned) {
278 279 280
        hasNonPositionedChildren = true;
        height = math.max(height, child.getMaxIntrinsicHeight(constraints));
      }
Hixie's avatar
Hixie committed
281 282
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
283 284 285 286 287 288 289 290 291 292 293 294
    }
    if (!hasNonPositionedChildren)
      return constraints.constrainHeight();
    assert(height == constraints.constrainHeight(height));
    return height;
  }

  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

  void performLayout() {
295
    _hasVisualOverflow = false;
296 297 298 299 300 301 302
    bool hasNonPositionedChildren = false;

    double width = 0.0;
    double height = 0.0;

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

Hixie's avatar
Hixie committed
305
      if (!childParentData.isPositioned) {
306 307 308
        hasNonPositionedChildren = true;

        child.layout(constraints, parentUsesSize: true);
Hixie's avatar
Hixie committed
309
        childParentData.position = Point.origin;
310 311 312 313 314 315

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

Hixie's avatar
Hixie committed
316
      child = childParentData.nextSibling;
317 318
    }

319
    if (hasNonPositionedChildren) {
320
      size = new Size(width, height);
321 322 323
      assert(size.width == constraints.constrainWidth(width));
      assert(size.height == constraints.constrainHeight(height));
    } else {
324
      size = constraints.biggest;
325
    }
326 327 328 329 330

    assert(!size.isInfinite);

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

Hixie's avatar
Hixie committed
333
      if (!childParentData.isPositioned) {
334 335
        double x = (size.width - child.size.width) * alignment.x;
        double y = (size.height - child.size.height) * alignment.y;
Hixie's avatar
Hixie committed
336
        childParentData.position = new Point(x, y);
Hans Muller's avatar
Hans Muller committed
337
      } else {
338
        BoxConstraints childConstraints = const BoxConstraints();
339

Hixie's avatar
Hixie committed
340 341
        if (childParentData.left != null && childParentData.right != null)
          childConstraints = childConstraints.tightenWidth(size.width - childParentData.right - childParentData.left);
342 343
        else if (childParentData.width != null)
          childConstraints = childConstraints.tightenWidth(childParentData.width);
344

Hixie's avatar
Hixie committed
345 346
        if (childParentData.top != null && childParentData.bottom != null)
          childConstraints = childConstraints.tightenHeight(size.height - childParentData.bottom - childParentData.top);
347 348
        else if (childParentData.height != null)
          childConstraints = childConstraints.tightenHeight(childParentData.height);
349 350 351 352

        child.layout(childConstraints, parentUsesSize: true);

        double x = 0.0;
Hixie's avatar
Hixie committed
353 354 355 356
        if (childParentData.left != null)
          x = childParentData.left;
        else if (childParentData.right != null)
          x = size.width - childParentData.right - child.size.width;
357 358 359

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

        double y = 0.0;
Hixie's avatar
Hixie committed
362 363 364 365
        if (childParentData.top != null)
          y = childParentData.top;
        else if (childParentData.bottom != null)
          y = size.height - childParentData.bottom - child.size.height;
366 367 368

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

Hixie's avatar
Hixie committed
370
        childParentData.position = new Point(x, y);
371 372
      }

Hixie's avatar
Hixie committed
373 374
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
375 376 377
    }
  }

Adam Barth's avatar
Adam Barth committed
378 379
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
380 381
  }

Hans Muller's avatar
Hans Muller committed
382 383
  void paintStack(PaintingContext context, Offset offset);

384
  void paint(PaintingContext context, Offset offset) {
385
    if (_hasVisualOverflow) {
386
      context.pushClipRect(needsCompositing, offset, Point.origin & size, paintStack);
387
    } else {
Hans Muller's avatar
Hans Muller committed
388
      paintStack(context, offset);
389
    }
390 391
  }
}
Hans Muller's avatar
Hans Muller committed
392 393 394 395 396 397 398 399 400 401 402

/// 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
/// initially layed out and 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
403
/// parameter. The left of each non-positioned child becomes the
Hans Muller's avatar
Hans Muller committed
404
/// difference between the child's width and the stack's width scaled by
405 406
/// alignment.x. The top of each non-positioned child is computed
/// similarly and scaled by alignement.y. So if the alignment x and y properties
Hans Muller's avatar
Hans Muller committed
407
/// are 0.0 (the default) then the non-positioned children remain in the
408
/// upper-left corner. If the alignment x and y properties are 0.5 then the
Hans Muller's avatar
Hans Muller committed
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
/// 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 deflating the width of the stack by the sum of the top and bottom values.
/// Similarly, if the child has rigth and left values that are both non-null,
/// the child is given a fixed width. Otherwise, the child is given unbounded
/// space in the non-fixed dimensions.
///
/// Once the child is laid out, the stack positions the child according to the
/// top, right, bottom, and left offsets. For example, if the top value is 10.0,
/// the top edge of the child will be placed 10.0 pixels from the top 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.
class RenderStack extends RenderStackBase {
  RenderStack({
    List<RenderBox> children,
426
    alignment: const FractionalOffset(0.0, 0.0)
Hans Muller's avatar
Hans Muller committed
427 428
  }) : super(
   children: children,
429
   alignment: alignment
Hans Muller's avatar
Hans Muller committed
430 431 432 433 434 435 436 437 438 439 440 441 442 443
 );

  void paintStack(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }
}

/// Implements the same layout algorithm as RenderStack but only paints the child
/// specified by index.
/// Note: although only one child is displayed, the cost of the layout algorithm is
/// still O(N), like an ordinary stack.
class RenderIndexedStack extends RenderStackBase {
  RenderIndexedStack({
    List<RenderBox> children,
444
    alignment: const FractionalOffset(0.0, 0.0),
Hans Muller's avatar
Hans Muller committed
445 446 447
    int index: 0
  }) : _index = index, super(
   children: children,
448
   alignment: alignment
Hans Muller's avatar
Hans Muller committed
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
  );

  int get index => _index;
  int _index;
  void set index (int value) {
    if (_index != value) {
      _index = value;
      markNeedsLayout();
    }
  }

  RenderBox _childAtIndex() {
    RenderBox child = firstChild;
    int i = 0;
    while (child != null && i < index) {
Hixie's avatar
Hixie committed
464 465
      final StackParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
Hans Muller's avatar
Hans Muller committed
466 467 468 469 470 471 472
      i += 1;
    }
    assert(i == index);
    assert(child != null);
    return child;
  }

Adam Barth's avatar
Adam Barth committed
473
  bool hitTestChildren(HitTestResult result, { Point position }) {
Hans Muller's avatar
Hans Muller committed
474
    if (firstChild == null)
Adam Barth's avatar
Adam Barth committed
475
      return false;
Hans Muller's avatar
Hans Muller committed
476 477
    assert(position != null);
    RenderBox child = _childAtIndex();
Hixie's avatar
Hixie committed
478 479 480
    final StackParentData childParentData = child.parentData;
    Point transformed = new Point(position.x - childParentData.position.x,
                                  position.y - childParentData.position.y);
Adam Barth's avatar
Adam Barth committed
481
    return child.hitTest(result, position: transformed);
Hans Muller's avatar
Hans Muller committed
482 483 484 485 486 487
  }

  void paintStack(PaintingContext context, Offset offset) {
    if (firstChild == null)
      return;
    RenderBox child = _childAtIndex();
Hixie's avatar
Hixie committed
488
    final StackParentData childParentData = child.parentData;
Adam Barth's avatar
Adam Barth committed
489
    context.paintChild(child, childParentData.offset + offset);
Hans Muller's avatar
Hans Muller committed
490 491
  }
}