shifted_box.dart 32.4 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:math' as math;
6

Adam Barth's avatar
Adam Barth committed
7 8
import 'package:meta/meta.dart';

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

13 14
/// Abstract class for one-child-layout render boxes that provide control over
/// the child's position.
15
abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
16
  /// Initializes the [child] property for sublasses.
17 18 19 20
  RenderShiftedBox(RenderBox child) {
    this.child = child;
  }

21
  @override
22
  double computeMinIntrinsicWidth(double height) {
23
    if (child != null)
24 25
      return child.getMinIntrinsicWidth(height);
    return 0.0;
26 27
  }

28
  @override
29
  double computeMaxIntrinsicWidth(double height) {
30
    if (child != null)
31 32
      return child.getMaxIntrinsicWidth(height);
    return 0.0;
33 34
  }

35
  @override
36
  double computeMinIntrinsicHeight(double width) {
37
    if (child != null)
38 39
      return child.getMinIntrinsicHeight(width);
    return 0.0;
40 41
  }

42
  @override
43
  double computeMaxIntrinsicHeight(double width) {
44
    if (child != null)
45 46
      return child.getMaxIntrinsicHeight(width);
    return 0.0;
47 48
  }

49
  @override
50 51 52 53 54
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    double result;
    if (child != null) {
      assert(!needsLayout);
      result = child.getDistanceToActualBaseline(baseline);
Hixie's avatar
Hixie committed
55
      final BoxParentData childParentData = child.parentData;
56
      if (result != null)
57
        result += childParentData.offset.dy;
58 59 60 61 62 63
    } else {
      result = super.computeDistanceToActualBaseline(baseline);
    }
    return result;
  }

64
  @override
65
  void paint(PaintingContext context, Offset offset) {
Hixie's avatar
Hixie committed
66 67
    if (child != null) {
      final BoxParentData childParentData = child.parentData;
Adam Barth's avatar
Adam Barth committed
68
      context.paintChild(child, childParentData.offset + offset);
Hixie's avatar
Hixie committed
69
    }
70 71
  }

72
  @override
Adam Barth's avatar
Adam Barth committed
73
  bool hitTestChildren(HitTestResult result, { Point position }) {
74
    if (child != null) {
Hixie's avatar
Hixie committed
75
      final BoxParentData childParentData = child.parentData;
76 77
      final Point childPosition = new Point(position.x - childParentData.offset.dx,
                                            position.y - childParentData.offset.dy);
Adam Barth's avatar
Adam Barth committed
78
      return child.hitTest(result, position: childPosition);
79
    }
Adam Barth's avatar
Adam Barth committed
80
    return false;
81 82 83 84
  }

}

85 86 87 88 89 90
/// Insets its child by the given padding.
///
/// When passing layout constraints to its child, padding shrinks the
/// constraints by the given padding, causing the child to layout at a smaller
/// size. Padding then sizes itself to its child's size, inflated by the
/// padding, effectively creating empty space around the child.
91
class RenderPadding extends RenderShiftedBox {
92 93 94
  /// Creates a render object that insets its child.
  ///
  /// The [padding] argument must not be null and must have non-negative insets.
95
  RenderPadding({
96
    EdgeInsets padding,
97 98
    RenderBox child
  }) : _padding = padding, super(child) {
99
    assert(padding != null);
100
    assert(padding.isNonNegative);
101 102
  }

103
  /// The amount to pad the child in each dimension.
104 105
  EdgeInsets get padding => _padding;
  EdgeInsets _padding;
106
  set padding (EdgeInsets value) {
107
    assert(value != null);
108
    assert(value.isNonNegative);
109 110 111 112 113 114
    if (_padding == value)
      return;
    _padding = value;
    markNeedsLayout();
  }

115
  @override
116
  double computeMinIntrinsicWidth(double height) {
117 118 119
    final double totalHorizontalPadding = padding.left + padding.right;
    final double totalVerticalPadding = padding.top + padding.bottom;
    if (child != null) // next line relies on double.INFINITY absorption
120
      return child.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
121
    return totalHorizontalPadding;
122 123
  }

124
  @override
125
  double computeMaxIntrinsicWidth(double height) {
126 127 128
    final double totalHorizontalPadding = padding.left + padding.right;
    final double totalVerticalPadding = padding.top + padding.bottom;
    if (child != null) // next line relies on double.INFINITY absorption
129
      return child.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
130
    return totalHorizontalPadding;
131 132
  }

133
  @override
134
  double computeMinIntrinsicHeight(double width) {
135 136 137
    final double totalHorizontalPadding = padding.left + padding.right;
    final double totalVerticalPadding = padding.top + padding.bottom;
    if (child != null) // next line relies on double.INFINITY absorption
138
      return child.getMinIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding;
139
    return totalVerticalPadding;
140 141
  }

142
  @override
143
  double computeMaxIntrinsicHeight(double width) {
144 145 146
    final double totalHorizontalPadding = padding.left + padding.right;
    final double totalVerticalPadding = padding.top + padding.bottom;
    if (child != null) // next line relies on double.INFINITY absorption
147
      return child.getMaxIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding;
148
    return totalVerticalPadding;
149 150
  }

151
  @override
152 153 154 155 156 157 158 159 160 161 162
  void performLayout() {
    assert(padding != null);
    if (child == null) {
      size = constraints.constrain(new Size(
        padding.left + padding.right,
        padding.top + padding.bottom
      ));
      return;
    }
    BoxConstraints innerConstraints = constraints.deflate(padding);
    child.layout(innerConstraints, parentUsesSize: true);
Hixie's avatar
Hixie committed
163
    final BoxParentData childParentData = child.parentData;
164
    childParentData.offset = new Offset(padding.left, padding.top);
165 166 167 168 169 170
    size = constraints.constrain(new Size(
      padding.left + child.size.width + padding.right,
      padding.top + child.size.height + padding.bottom
    ));
  }

171
  @override
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
  void debugPaintSize(PaintingContext context, Offset offset) {
    super.debugPaintSize(context, offset);
    assert(() {
      Paint paint;
      if (child != null && !child.size.isEmpty) {
        Path path;
        paint = new Paint()
          ..color = debugPaintPaddingColor;
        path = new Path()
          ..moveTo(offset.dx, offset.dy)
          ..lineTo(offset.dx + size.width, offset.dy)
          ..lineTo(offset.dx + size.width, offset.dy + size.height)
          ..lineTo(offset.dx, offset.dy + size.height)
          ..close()
          ..moveTo(offset.dx + padding.left, offset.dy + padding.top)
          ..lineTo(offset.dx + padding.left, offset.dy + size.height - padding.bottom)
          ..lineTo(offset.dx + size.width - padding.right, offset.dy + size.height - padding.bottom)
          ..lineTo(offset.dx + size.width - padding.right, offset.dy + padding.top)
          ..close();
        context.canvas.drawPath(path, paint);
        paint = new Paint()
          ..color = debugPaintPaddingInnerEdgeColor;
194
        const double kOutline = 2.0;
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
        path = new Path()
          ..moveTo(offset.dx + math.max(padding.left - kOutline, 0.0), offset.dy + math.max(padding.top - kOutline, 0.0))
          ..lineTo(offset.dx + math.min(size.width - padding.right + kOutline, size.width), offset.dy + math.max(padding.top - kOutline, 0.0))
          ..lineTo(offset.dx + math.min(size.width - padding.right + kOutline, size.width), offset.dy + math.min(size.height - padding.bottom + kOutline, size.height))
          ..lineTo(offset.dx + math.max(padding.left - kOutline, 0.0), offset.dy + math.min(size.height - padding.bottom + kOutline, size.height))
          ..close()
          ..moveTo(offset.dx + padding.left, offset.dy + padding.top)
          ..lineTo(offset.dx + padding.left, offset.dy + size.height - padding.bottom)
          ..lineTo(offset.dx + size.width - padding.right, offset.dy + size.height - padding.bottom)
          ..lineTo(offset.dx + size.width - padding.right, offset.dy + padding.top)
          ..close();
        context.canvas.drawPath(path, paint);
      } else {
        paint = new Paint()
          ..color = debugPaintSpacingColor;
        context.canvas.drawRect(offset & size, paint);
      }
      return true;
    });
  }

216
  @override
217 218 219
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('padding: $padding');
220
  }
221 222
}

223 224
/// Abstract class for one-child-layout render boxes that use a
/// [FractionalOffset] to align their children.
225
abstract class RenderAligningShiftedBox extends RenderShiftedBox {
226 227 228
  /// Initializes member variables for subclasses.
  ///
  /// The [alignment] argument must not be null.
229
  RenderAligningShiftedBox({
230 231 232
    FractionalOffset alignment: FractionalOffset.center,
    RenderBox child
  }) : _alignment = alignment, super(child) {
233
    assert(alignment != null && alignment.dx != null && alignment.dy != null);
234 235
  }

236 237 238 239 240 241 242 243 244
  /// How to align the child.
  ///
  /// The x and y values of the alignment control the horizontal and vertical
  /// alignment, respectively.  An x value of 0.0 means that the left edge of
  /// the child is aligned with the left edge of the parent whereas an x value
  /// of 1.0 means that the right edge of the child is aligned with the right
  /// edge of the parent. Other values interpolate (and extrapolate) linearly.
  /// For example, a value of 0.5 means that the center of the child is aligned
  /// with the center of the parent.
245 246
  FractionalOffset get alignment => _alignment;
  FractionalOffset _alignment;
247 248 249
  /// Sets the alignment to a new value, and triggers a layout update.
  ///
  /// The new alignment must not be null or have any null properties.
250
  set alignment (FractionalOffset newAlignment) {
251
    assert(newAlignment != null && newAlignment.dx != null && newAlignment.dy != null);
252
    if (_alignment == newAlignment)
253
      return;
254
    _alignment = newAlignment;
255 256 257
    markNeedsLayout();
  }

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
  /// Apply the current [alignment] to the [child].
  ///
  /// Subclasses should call this method if they have a child, to have
  /// this class perform the actual alignment. If there is no child,
  /// do not call this method.
  ///
  /// This method must be called after the child has been laid out and
  /// this object's own size has been set.
  void alignChild() {
    assert(child != null);
    assert(!child.needsLayout);
    assert(child.hasSize);
    assert(hasSize);
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = alignment.alongOffset(size - child.size);
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('alignment: $alignment');
  }
}

282
/// Positions its child using a [FractionalOffset].
283 284 285
///
/// For example, to align a box at the bottom right, you would pass this box a
/// tight constraint that is bigger than the child's natural size,
Adam Barth's avatar
Adam Barth committed
286
/// with an alignment of [FractionalOffset.bottomRight].
287 288 289 290 291 292
///
/// By default, sizes to be as big as possible in both axes. If either axis is
/// unconstrained, then in that direction it will be sized to fit the child's
/// dimensions. Using widthFactor and heightFactor you can force this latter
/// behavior in all cases.
class RenderPositionedBox extends RenderAligningShiftedBox {
293
  /// Creates a render object that positions its child.
294 295 296 297
  RenderPositionedBox({
    RenderBox child,
    double widthFactor,
    double heightFactor,
Adam Barth's avatar
Adam Barth committed
298
    FractionalOffset alignment: FractionalOffset.center
299 300 301 302 303 304 305
  }) : _widthFactor = widthFactor,
       _heightFactor = heightFactor,
       super(child: child, alignment: alignment) {
    assert(widthFactor == null || widthFactor >= 0.0);
    assert(heightFactor == null || heightFactor >= 0.0);
  }

306 307 308
  /// If non-null, sets its width to the child's width multipled by this factor.
  ///
  /// Can be both greater and less than 1.0 but must be positive.
309
  double get widthFactor => _widthFactor;
310
  double _widthFactor;
311
  set widthFactor (double value) {
312
    assert(value == null || value >= 0.0);
313
    if (_widthFactor == value)
314
      return;
315
    _widthFactor = value;
316 317 318
    markNeedsLayout();
  }

319 320 321
  /// If non-null, sets its height to the child's height multipled by this factor.
  ///
  /// Can be both greater and less than 1.0 but must be positive.
322
  double get heightFactor => _heightFactor;
323
  double _heightFactor;
324
  set heightFactor (double value) {
325
    assert(value == null || value >= 0.0);
326 327 328 329 330
    if (_heightFactor == value)
      return;
    _heightFactor = value;
    markNeedsLayout();
  }
331

332
  @override
333
  void performLayout() {
334 335
    final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.INFINITY;
    final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.INFINITY;
336

337 338
    if (child != null) {
      child.layout(constraints.loosen(), parentUsesSize: true);
339 340
      size = constraints.constrain(new Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.INFINITY,
                                            shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.INFINITY));
341
      alignChild();
342
    } else {
343 344
      size = constraints.constrain(new Size(shrinkWrapWidth ? 0.0 : double.INFINITY,
                                            shrinkWrapHeight ? 0.0 : double.INFINITY));
345 346 347
    }
  }

348
  @override
349 350 351 352 353 354 355
  void debugPaintSize(PaintingContext context, Offset offset) {
    super.debugPaintSize(context, offset);
    assert(() {
      Paint paint;
      if (child != null && !child.size.isEmpty) {
        Path path;
        paint = new Paint()
356
          ..style = PaintingStyle.stroke
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
          ..strokeWidth = 1.0
          ..color = debugPaintArrowColor;
        path = new Path();
        final BoxParentData childParentData = child.parentData;
        if (childParentData.offset.dy > 0.0) {
          // vertical alignment arrows
          double headSize = math.min(childParentData.offset.dy * 0.2, 10.0);
          path
            ..moveTo(offset.dx + size.width / 2.0, offset.dy)
            ..relativeLineTo(0.0, childParentData.offset.dy - headSize)
            ..relativeLineTo(headSize, 0.0)
            ..relativeLineTo(-headSize, headSize)
            ..relativeLineTo(-headSize, -headSize)
            ..relativeLineTo(headSize, 0.0)
            ..moveTo(offset.dx + size.width / 2.0, offset.dy + size.height)
            ..relativeLineTo(0.0, -childParentData.offset.dy + headSize)
            ..relativeLineTo(headSize, 0.0)
            ..relativeLineTo(-headSize, -headSize)
            ..relativeLineTo(-headSize, headSize)
            ..relativeLineTo(headSize, 0.0);
          context.canvas.drawPath(path, paint);
        }
        if (childParentData.offset.dx > 0.0) {
          // horizontal alignment arrows
          double headSize = math.min(childParentData.offset.dx * 0.2, 10.0);
          path
            ..moveTo(offset.dx, offset.dy + size.height / 2.0)
            ..relativeLineTo(childParentData.offset.dx - headSize, 0.0)
            ..relativeLineTo(0.0, headSize)
            ..relativeLineTo(headSize, -headSize)
            ..relativeLineTo(-headSize, -headSize)
            ..relativeLineTo(0.0, headSize)
            ..moveTo(offset.dx + size.width, offset.dy + size.height / 2.0)
            ..relativeLineTo(-childParentData.offset.dx + headSize, 0.0)
            ..relativeLineTo(0.0, headSize)
            ..relativeLineTo(-headSize, -headSize)
            ..relativeLineTo(headSize, -headSize)
            ..relativeLineTo(0.0, headSize);
          context.canvas.drawPath(path, paint);
        }
      } else {
        paint = new Paint()
          ..color = debugPaintSpacingColor;
        context.canvas.drawRect(offset & size, paint);
      }
      return true;
    });
  }

406
  @override
407 408
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
409 410
    description.add('widthFactor: ${_widthFactor ?? "expand"}');
    description.add('heightFactor: ${_heightFactor ?? "expand"}');
411
  }
412 413
}

414 415 416 417 418 419 420 421 422 423 424 425 426
/// A render object that imposes different constraints on its child than it gets
/// from its parent, possibly allowing the child to overflow the parent.
///
/// A render overflow box proxies most functions in the render box protocol to
/// its child, except that when laying out its child, it passes constraints
/// based on the minWidth, maxWidth, minHeight, and maxHeight fields instead of
/// just passing the parent's constraints in. Specifically, it overrides any of
/// the equivalent fields on the constraints given by the parent with the
/// constraints given by these fields for each such field that is not null. It
/// then sizes itself based on the parent's constraints' maxWidth and maxHeight,
/// ignoring the child's dimensions.
///
/// For example, if you wanted a box to always render 50 pixels high, regardless
427 428 429 430
/// of where it was rendered, you would wrap it in a
/// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0.
/// Generally speaking, to avoid confusing behavior around hit testing, a
/// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect.
431
///
432
/// The child is positioned according to [alignment]. To position a smaller
433
/// child inside a larger parent, use [RenderPositionedBox] and
434
/// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox.
435
class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
436
  /// Creates a render object that lets its child overflow itself.
437
  RenderConstrainedOverflowBox({
438 439 440 441 442
    RenderBox child,
    double minWidth,
    double maxWidth,
    double minHeight,
    double maxHeight,
Adam Barth's avatar
Adam Barth committed
443
    FractionalOffset alignment: FractionalOffset.center
444 445 446 447
  }) : _minWidth = minWidth,
       _maxWidth = maxWidth,
       _minHeight = minHeight,
       _maxHeight = maxHeight,
448
       super(child: child, alignment: alignment);
449 450 451 452 453

  /// The minimum width constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double get minWidth => _minWidth;
  double _minWidth;
454
  set minWidth (double value) {
455 456 457 458 459 460 461 462 463 464
    if (_minWidth == value)
      return;
    _minWidth = value;
    markNeedsLayout();
  }

  /// The maximum width constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double get maxWidth => _maxWidth;
  double _maxWidth;
465
  set maxWidth (double value) {
466 467 468 469 470 471 472 473 474 475
    if (_maxWidth == value)
      return;
    _maxWidth = value;
    markNeedsLayout();
  }

  /// The minimum height constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double get minHeight => _minHeight;
  double _minHeight;
476
  set minHeight (double value) {
477 478 479 480 481 482 483 484 485 486
    if (_minHeight == value)
      return;
    _minHeight = value;
    markNeedsLayout();
  }

  /// The maximum height constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double get maxHeight => _maxHeight;
  double _maxHeight;
487
  set maxHeight (double value) {
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
    if (_maxHeight == value)
      return;
    _maxHeight = value;
    markNeedsLayout();
  }

  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
    return new BoxConstraints(
      minWidth: _minWidth ?? constraints.minWidth,
      maxWidth: _maxWidth ?? constraints.maxWidth,
      minHeight: _minHeight ?? constraints.minHeight,
      maxHeight: _maxHeight ?? constraints.maxHeight
    );
  }

503
  @override
504 505
  bool get sizedByParent => true;

506
  @override
507 508 509 510
  void performResize() {
    size = constraints.biggest;
  }

511
  @override
512 513 514
  void performLayout() {
    if (child != null) {
      child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
515
      alignChild();
516 517 518
    }
  }

519
  @override
520 521 522 523 524 525
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('minWidth: ${minWidth ?? "use parent minWidth constraint"}');
    description.add('maxWidth: ${maxWidth ?? "use parent maxWidth constraint"}');
    description.add('minHeight: ${minHeight ?? "use parent minHeight constraint"}');
    description.add('maxHeight: ${maxHeight ?? "use parent maxHeight constraint"}');
526 527 528
  }
}

529
/// A render box that is a specific size but passes its original constraints
530 531
/// through to its child, which will probably overflow.
class RenderSizedOverflowBox extends RenderAligningShiftedBox {
532 533 534
  /// Creates a render box of a given size that lets its child overflow.
  ///
  /// The [requestedSize] argument must not be null.
535 536 537
  RenderSizedOverflowBox({
    RenderBox child,
    Size requestedSize,
Adam Barth's avatar
Adam Barth committed
538
    FractionalOffset alignment: FractionalOffset.center
539 540 541 542 543 544 545 546
  }) : _requestedSize = requestedSize,
       super(child: child, alignment: alignment) {
    assert(requestedSize != null);
  }

  /// The size this render box should attempt to be.
  Size get requestedSize => _requestedSize;
  Size _requestedSize;
547
  set requestedSize (Size value) {
548 549 550 551 552 553 554 555
    assert(value != null);
    if (_requestedSize == value)
      return;
    _requestedSize = value;
    markNeedsLayout();
  }

  @override
556
  double computeMinIntrinsicWidth(double height) {
557
    return _requestedSize.width;
558 559 560
  }

  @override
561
  double computeMaxIntrinsicWidth(double height) {
562
    return _requestedSize.width;
563 564 565
  }

  @override
566
  double computeMinIntrinsicHeight(double width) {
567
    return _requestedSize.height;
568 569 570
  }

  @override
571
  double computeMaxIntrinsicHeight(double width) {
572
    return _requestedSize.height;
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
  }

  @override
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    if (child != null)
      return child.getDistanceToActualBaseline(baseline);
    return super.computeDistanceToActualBaseline(baseline);
  }

  @override
  void performLayout() {
    size = constraints.constrain(_requestedSize);
    if (child != null) {
      child.layout(constraints);
      alignChild();
    }
  }
}

/// Sizes its child to a fraction of the total available space.
///
/// For both its width and height, this render object imposes a tight
/// constraint on its child that is a multiple (typically less than 1.0) of the
/// maximum constraint it received from its parent on that axis. If the factor
/// for a given axis is null, then the constraints from the parent are just
/// passed through instead.
///
/// It then tries to size itself to the size of its child.
class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
602 603 604 605
  /// Creates a render box that sizes its child to a fraction of the total available space.
  ///
  /// If non-null, the [widthFactor] and [heightFactor] arguments must be
  /// non-negative.
606 607 608 609
  RenderFractionallySizedOverflowBox({
    RenderBox child,
    double widthFactor,
    double heightFactor,
Adam Barth's avatar
Adam Barth committed
610
    FractionalOffset alignment: FractionalOffset.center
611 612 613 614 615 616 617 618 619 620 621
  }) : _widthFactor = widthFactor,
       _heightFactor = heightFactor,
       super(child: child, alignment: alignment) {
    assert(_widthFactor == null || _widthFactor >= 0.0);
    assert(_heightFactor == null || _heightFactor >= 0.0);
  }

  /// If non-null, the factor of the incoming width to use.
  ///
  /// If non-null, the child is given a tight width constraint that is the max
  /// incoming width constraint multipled by this factor.  If null, the child is
622
  /// given the incoming width constraints.
623 624
  double get widthFactor => _widthFactor;
  double _widthFactor;
625
  set widthFactor (double value) {
626 627 628 629 630 631 632 633 634 635 636
    assert(value == null || value >= 0.0);
    if (_widthFactor == value)
      return;
    _widthFactor = value;
    markNeedsLayout();
  }

  /// If non-null, the factor of the incoming height to use.
  ///
  /// If non-null, the child is given a tight height constraint that is the max
  /// incoming width constraint multipled by this factor.  If null, the child is
637
  /// given the incoming width constraints.
638 639
  double get heightFactor => _heightFactor;
  double _heightFactor;
640
  set heightFactor (double value) {
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
    assert(value == null || value >= 0.0);
    if (_heightFactor == value)
      return;
    _heightFactor = value;
    markNeedsLayout();
  }

  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
    double minWidth = constraints.minWidth;
    double maxWidth = constraints.maxWidth;
    if (_widthFactor != null) {
      double width = maxWidth * _widthFactor;
      minWidth = width;
      maxWidth = width;
    }
    double minHeight = constraints.minHeight;
    double maxHeight = constraints.maxHeight;
    if (_heightFactor != null) {
      double height = maxHeight * _heightFactor;
      minHeight = height;
      maxHeight = height;
    }
    return new BoxConstraints(
      minWidth: minWidth,
      maxWidth: maxWidth,
      minHeight: minHeight,
      maxHeight: maxHeight
    );
  }

  @override
672
  double computeMinIntrinsicWidth(double height) {
673 674
    double result;
    if (child == null) {
675
      result = super.computeMinIntrinsicWidth(height);
676 677 678 679 680
    } else { // the following line relies on double.INFINITY absorption
      result = child.getMinIntrinsicWidth(height * (_heightFactor ?? 1.0));
    }
    assert(result.isFinite);
    return result / (_widthFactor ?? 1.0);
681 682 683
  }

  @override
684
  double computeMaxIntrinsicWidth(double height) {
685 686
    double result;
    if (child == null) {
687
      result = super.computeMaxIntrinsicWidth(height);
688 689 690 691 692
    } else { // the following line relies on double.INFINITY absorption
      result = child.getMaxIntrinsicWidth(height * (_heightFactor ?? 1.0));
    }
    assert(result.isFinite);
    return result / (_widthFactor ?? 1.0);
693 694 695
  }

  @override
696
  double computeMinIntrinsicHeight(double width) {
697 698
    double result;
    if (child == null) {
699
      result = super.computeMinIntrinsicHeight(width);
700 701 702 703 704
    } else { // the following line relies on double.INFINITY absorption
      result = child.getMinIntrinsicHeight(width * (_widthFactor ?? 1.0));
    }
    assert(result.isFinite);
    return result / (_heightFactor ?? 1.0);
705 706 707
  }

  @override
708
  double computeMaxIntrinsicHeight(double width) {
709 710
    double result;
    if (child == null) {
711
      result = super.computeMaxIntrinsicHeight(width);
712 713 714 715 716
    } else { // the following line relies on double.INFINITY absorption
      result = child.getMaxIntrinsicHeight(width * (_widthFactor ?? 1.0));
    }
    assert(result.isFinite);
    return result / (_heightFactor ?? 1.0);
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
  }

  @override
  void performLayout() {
    if (child != null) {
      child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child.size);
      alignChild();
    } else {
      size = constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero));
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('widthFactor: ${_widthFactor ?? "pass-through"}');
    description.add('heightFactor: ${_heightFactor ?? "pass-through"}');
735 736 737
  }
}

Adam Barth's avatar
Adam Barth committed
738
/// A delegate for computing the layout of a render object with a single child.
739
class SingleChildLayoutDelegate {
740
  /// Returns the size of this object given the incoming constraints.
Adam Barth's avatar
Adam Barth committed
741 742
  Size getSize(BoxConstraints constraints) => constraints.biggest;

743
  /// Returns the box constraints for the child given the incoming constraints.
Adam Barth's avatar
Adam Barth committed
744 745 746
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints;

  /// Returns the position where the child should be placed given the size of this object and the size of the child.
747
  Offset getPositionForChild(Size size, Size childSize) => Offset.zero;
748 749

  /// Override this method to return true when the child needs to be laid out.
Adam Barth's avatar
Adam Barth committed
750
  bool shouldRelayout(@checked SingleChildLayoutDelegate oldDelegate) => true;
Adam Barth's avatar
Adam Barth committed
751 752
}

753 754 755 756 757 758
/// Defers the layout of its single child to a delegate.
///
/// The delegate can determine the layout constraints for the child and can
/// decide where to position the child. The delegate can also determine the size
/// of the parent, but the size of the parent cannot depend on the size of the
/// child.
759
class RenderCustomSingleChildLayoutBox extends RenderShiftedBox {
760 761 762
  /// Creates a render box that defers its layout to a delgate.
  ///
  /// The [delegate] argument must not be null.
763
  RenderCustomSingleChildLayoutBox({
Adam Barth's avatar
Adam Barth committed
764
    RenderBox child,
765
    SingleChildLayoutDelegate delegate
Adam Barth's avatar
Adam Barth committed
766 767 768 769
  }) : _delegate = delegate, super(child) {
    assert(delegate != null);
  }

770
  /// A delegate that controls this object's layout.
771 772
  SingleChildLayoutDelegate get delegate => _delegate;
  SingleChildLayoutDelegate _delegate;
773
  set delegate (SingleChildLayoutDelegate newDelegate) {
Adam Barth's avatar
Adam Barth committed
774 775 776
    assert(newDelegate != null);
    if (_delegate == newDelegate)
      return;
777 778
    if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate))
      markNeedsLayout();
Adam Barth's avatar
Adam Barth committed
779 780 781 782 783 784 785
    _delegate = newDelegate;
  }

  Size _getSize(BoxConstraints constraints) {
    return constraints.constrain(_delegate.getSize(constraints));
  }

786 787 788 789
  // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
  // figure out the intrinsic dimensions. We really should either not support intrinsics,
  // or we should expose intrinsic delegate callbacks and throw if they're not implemented.

790
  @override
791
  double computeMinIntrinsicWidth(double height) {
792 793 794 795
    final double width = _getSize(new BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
Adam Barth's avatar
Adam Barth committed
796 797
  }

798
  @override
799
  double computeMaxIntrinsicWidth(double height) {
800 801 802 803
    final double width = _getSize(new BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
Adam Barth's avatar
Adam Barth committed
804 805
  }

806
  @override
807
  double computeMinIntrinsicHeight(double width) {
808 809 810 811
    final double height = _getSize(new BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite)
      return height;
    return 0.0;
Adam Barth's avatar
Adam Barth committed
812 813
  }

814
  @override
815
  double computeMaxIntrinsicHeight(double width) {
816 817 818 819
    final double height = _getSize(new BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite)
      return height;
    return 0.0;
Adam Barth's avatar
Adam Barth committed
820 821
  }

822
  @override
Adam Barth's avatar
Adam Barth committed
823 824
  bool get sizedByParent => true;

825
  @override
Adam Barth's avatar
Adam Barth committed
826 827 828 829
  void performResize() {
    size = _getSize(constraints);
  }

830
  @override
Adam Barth's avatar
Adam Barth committed
831 832
  void performLayout() {
    if (child != null) {
833
      BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints);
834
      assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
835
      child.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
Adam Barth's avatar
Adam Barth committed
836
      final BoxParentData childParentData = child.parentData;
837
      childParentData.offset = delegate.getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child.size);
Adam Barth's avatar
Adam Barth committed
838 839 840 841
    }
  }
}

842 843 844
/// Shifts the child down such that the child's baseline (or the
/// bottom of the child, if the child has no baseline) is [baseline]
/// logical pixels below the top of this box, then sizes this box to
845 846 847 848 849 850 851 852 853 854 855 856 857
/// contain the child.
///
/// If [baseline] is less than the distance from the top of the child
/// to the baseline of the child, then the child will overflow the top
/// of the box. This is typically not desireable, in particular, that
/// part of the child will not be found when doing hit tests, so the
/// user cannot interact with that part of the child.
///
/// This box will be sized so that its bottom is coincident with the
/// bottom of the child. This means if this box shifts the child down,
/// there will be space between the top of this box and the top of the
/// child, but there is never space between the bottom of the child
/// and the bottom of the box.
858
class RenderBaseline extends RenderShiftedBox {
859 860
  /// Creates a [RenderBaseline] object.
  ///
861
  /// The [baseline] and [baselineType] arguments must not be null.
862 863 864 865 866 867 868 869 870 871 872
  RenderBaseline({
    RenderBox child,
    double baseline,
    TextBaseline baselineType
  }) : _baseline = baseline,
       _baselineType = baselineType,
       super(child) {
    assert(baseline != null);
    assert(baselineType != null);
  }

873 874
  /// The number of logical pixels from the top of this box at which to position
  /// the child's baseline.
875
  double get baseline => _baseline;
876
  double _baseline;
877
  set baseline (double value) {
878 879 880 881 882 883 884
    assert(value != null);
    if (_baseline == value)
      return;
    _baseline = value;
    markNeedsLayout();
  }

885
  /// The type of baseline to use for positioning the child.
886
  TextBaseline get baselineType => _baselineType;
887
  TextBaseline _baselineType;
888
  set baselineType (TextBaseline value) {
889 890 891 892 893 894 895
    assert(value != null);
    if (_baselineType == value)
      return;
    _baselineType = value;
    markNeedsLayout();
  }

896
  @override
897 898 899
  void performLayout() {
    if (child != null) {
      child.layout(constraints.loosen(), parentUsesSize: true);
900
      final double childBaseline = child.getDistanceToBaseline(baselineType);
901
      final double actualBaseline = baseline;
902
      final double top = actualBaseline - childBaseline;
Hixie's avatar
Hixie committed
903
      final BoxParentData childParentData = child.parentData;
904 905 906
      childParentData.offset = new Offset(0.0, top);
      final Size childSize = child.size;
      size = constraints.constrain(new Size(childSize.width, top + childSize.height));
907 908 909 910 911
    } else {
      performResize();
    }
  }

912
  @override
913 914 915 916
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('baseline: $baseline');
    description.add('baselineType: $baselineType');
917
  }
918
}