block.dart 15.9 KB
Newer Older
1 2 3 4 5 6
// 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;

7
import 'package:vector_math/vector_math_64.dart';
8 9 10

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

Florian Loitsch's avatar
Florian Loitsch committed
13
/// Parent data for use with [RenderBlockBase].
Hixie's avatar
Hixie committed
14
class BlockParentData extends ContainerBoxParentDataMixin<RenderBox> { }
15

16
typedef double _ChildSizingFunction(RenderBox child, BoxConstraints constraints);
17
typedef double _Constrainer(double value);
18

Florian Loitsch's avatar
Florian Loitsch committed
19
/// Implements the block layout algorithm.
20 21 22 23 24 25 26 27
///
/// In block layout, children are arranged linearly along the main axis (either
/// horizontally or vertically). In the cross axis, children are stretched to
/// match the block's cross-axis extent. In the main axis, children are given
/// unlimited space and the block expands its main axis to contain all its
/// children. Because blocks expand in the main axis, blocks must be given
/// unlimited space in the main axis, typically by being contained in a
/// viewport with a scrolling direction that matches the block's main axis.
28 29 30
abstract class RenderBlockBase extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, BlockParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData>
31
    implements HasScrollDirection {
32 33

  RenderBlockBase({
34
    List<RenderBox> children,
35
    Axis direction: Axis.vertical,
36 37 38
    double itemExtent,
    double minExtent: 0.0
  }) : _direction = direction, _itemExtent = itemExtent, _minExtent = minExtent {
39 40 41 42 43 44 45 46
    addAll(children);
  }

  void setupParentData(RenderBox child) {
    if (child.parentData is! BlockParentData)
      child.parentData = new BlockParentData();
  }

Florian Loitsch's avatar
Florian Loitsch committed
47
  /// The direction to use as the main axis.
48 49 50
  Axis get direction => _direction;
  Axis _direction;
  void set direction (Axis value) {
51 52 53 54 55 56
    if (_direction != value) {
      _direction = value;
      markNeedsLayout();
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
57
  /// If non-null, forces children to be exactly this large in the main axis.
58
  double get itemExtent => _itemExtent;
59
  double _itemExtent;
60 61 62 63 64 65 66
  void set itemExtent(double value) {
    if (value != _itemExtent) {
      _itemExtent = value;
      markNeedsLayout();
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
67
  /// Forces the block to be at least this large in the main-axis.
68
  double get minExtent => _minExtent;
69
  double _minExtent;
70 71 72 73 74 75 76
  void set minExtent(double value) {
    if (value != _minExtent) {
      _minExtent = value;
      markNeedsLayout();
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
77
  /// Whether the main axis is vertical.
78
  bool get isVertical => _direction == Axis.vertical;
79

80
  Axis get scrollDirection => _direction;
81

82
  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
83
    if (isVertical)
84 85 86 87
      return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth),
                                         height: itemExtent);
    return new BoxConstraints.tightFor(height: constraints.constrainHeight(constraints.maxHeight),
                                       width: itemExtent);
88 89
  }

90 91 92
  double get _mainAxisExtent {
    RenderBox child = lastChild;
    if (child == null)
93
      return minExtent;
94
    BoxParentData parentData = child.parentData;
95
    return isVertical ?
96 97
        math.max(minExtent, parentData.offset.dy + child.size.height) :
        math.max(minExtent, parentData.offset.dx + child.size.width);
98 99
  }

100
  void performLayout() {
101 102
    BoxConstraints innerConstraints = _getInnerConstraints(constraints);
    double position = 0.0;
103 104 105
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(innerConstraints, parentUsesSize: true);
Hixie's avatar
Hixie committed
106
      final BlockParentData childParentData = child.parentData;
107
      childParentData.offset = isVertical ? new Offset(0.0, position) : new Offset(position, 0.0);
108
      position += isVertical ? child.size.height : child.size.width;
Hixie's avatar
Hixie committed
109 110
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
111
    }
112
    size = isVertical ?
113 114 115
        constraints.constrain(new Size(constraints.maxWidth, _mainAxisExtent)) :
        constraints.constrain(new Size(_mainAxisExtent, constraints.maxHeight));
    assert(!size.isInfinite);
116 117
  }

118 119 120
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('direction: $direction');
121
  }
122 123
}

Florian Loitsch's avatar
Florian Loitsch committed
124
/// A block layout with a concrete set of children.
125 126
class RenderBlock extends RenderBlockBase {

127
  RenderBlock({
128
    List<RenderBox> children,
129
    Axis direction: Axis.vertical,
130 131 132
    double itemExtent,
    double minExtent: 0.0
  }) : super(children: children, direction: direction, itemExtent: itemExtent, minExtent: minExtent);
133

134
  double _getIntrinsicCrossAxis(BoxConstraints constraints, _ChildSizingFunction childSize, _Constrainer constrainer) {
135
    double extent = 0.0;
136
    BoxConstraints innerConstraints = isVertical ? constraints.widthConstraints() : constraints.heightConstraints();
137 138
    RenderBox child = firstChild;
    while (child != null) {
139
      extent = math.max(extent, childSize(child, innerConstraints));
Hixie's avatar
Hixie committed
140 141
      final BlockParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
142
    }
143
    return constrainer(extent);
144 145
  }

146
  double _getIntrinsicMainAxis(BoxConstraints constraints, _Constrainer constrainer) {
147 148
    double extent = 0.0;
    BoxConstraints innerConstraints = _getInnerConstraints(constraints);
149 150
    RenderBox child = firstChild;
    while (child != null) {
151
      double childExtent = isVertical ?
152 153 154
        child.getMinIntrinsicHeight(innerConstraints) :
        child.getMinIntrinsicWidth(innerConstraints);
      extent += childExtent;
Hixie's avatar
Hixie committed
155 156
      final BlockParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
157
    }
158
    return constrainer(math.max(extent, minExtent));
159 160
  }

161
  double getMinIntrinsicWidth(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
162
    assert(constraints.debugAssertIsNormalized);
163
    if (isVertical) {
Hixie's avatar
Hixie committed
164 165
      return _getIntrinsicCrossAxis(
        constraints,
166 167
        (RenderBox child, BoxConstraints innerConstraints) => child.getMinIntrinsicWidth(innerConstraints),
        constraints.constrainWidth
Hixie's avatar
Hixie committed
168
      );
169
    }
170
    return _getIntrinsicMainAxis(constraints, constraints.constrainWidth);
171 172 173
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
174
    assert(constraints.debugAssertIsNormalized);
175
    if (isVertical) {
Hixie's avatar
Hixie committed
176 177
      return _getIntrinsicCrossAxis(
        constraints,
178 179
        (RenderBox child, BoxConstraints innerConstraints) => child.getMaxIntrinsicWidth(innerConstraints),
        constraints.constrainWidth
Hixie's avatar
Hixie committed
180
      );
181
    }
182
    return _getIntrinsicMainAxis(constraints, constraints.constrainWidth);
183 184 185
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
186
    assert(constraints.debugAssertIsNormalized);
187
    if (isVertical)
188
      return _getIntrinsicMainAxis(constraints, constraints.constrainHeight);
Hixie's avatar
Hixie committed
189 190
    return _getIntrinsicCrossAxis(
      constraints,
191 192
      (RenderBox child, BoxConstraints innerConstraints) => child.getMinIntrinsicWidth(innerConstraints),
      constraints.constrainHeight
Hixie's avatar
Hixie committed
193
    );
194 195 196
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
197
    assert(constraints.debugAssertIsNormalized);
198
    if (isVertical)
199
      return _getIntrinsicMainAxis(constraints, constraints.constrainHeight);
Hixie's avatar
Hixie committed
200 201
    return _getIntrinsicCrossAxis(
      constraints,
202 203
      (RenderBox child, BoxConstraints innerConstraints) => child.getMaxIntrinsicWidth(innerConstraints),
      constraints.constrainHeight
Hixie's avatar
Hixie committed
204
    );
205 206 207 208 209 210 211
  }

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

  void performLayout() {
212
    assert((isVertical ? constraints.maxHeight >= double.INFINITY : constraints.maxWidth >= double.INFINITY) &&
Florian Loitsch's avatar
Florian Loitsch committed
213
           'RenderBlock does not clip or resize its children, so it must be placed in a parent that does not constrain '
214
           'the block\'s main direction. You probably want to put the RenderBlock inside a RenderViewport.' is String);
215 216 217
    super.performLayout();
  }

218 219
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
220 221
  }

Adam Barth's avatar
Adam Barth committed
222 223
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
224 225 226 227
  }

}

Florian Loitsch's avatar
Florian Loitsch committed
228
/// A block layout whose children depend on its layout.
229 230 231 232 233 234 235
///
/// This class invokes a callbacks for layout and intrinsic dimensions. The main
/// [callback] (constructor argument and property) is expected to modify the
/// element's child list. The regular block layout algorithm is then applied to
/// the children. The intrinsic dimension callbacks are called to determine
/// intrinsic dimensions; if no value can be returned, they should not be set
/// or, if set, should return null.
236 237 238 239
class RenderBlockViewport extends RenderBlockBase {

  RenderBlockViewport({
    LayoutCallback callback,
240
    VoidCallback postLayoutCallback,
241 242 243
    ExtentCallback totalExtentCallback,
    ExtentCallback maxCrossAxisDimensionCallback,
    ExtentCallback minCrossAxisDimensionCallback,
244
    Painter overlayPainter,
245
    Axis direction: Axis.vertical,
246 247
    double itemExtent,
    double minExtent: 0.0,
248
    double startOffset: 0.0,
249
    List<RenderBox> children
250 251
  }) : _callback = callback,
       _totalExtentCallback = totalExtentCallback,
252 253
       _maxCrossAxisExtentCallback = maxCrossAxisDimensionCallback,
       _minCrossAxisExtentCallback = minCrossAxisDimensionCallback,
254
       _overlayPainter = overlayPainter,
255
       _startOffset = startOffset,
256
       super(children: children, direction: direction, itemExtent: itemExtent, minExtent: minExtent);
257 258

  bool _inCallback = false;
259
  bool get isRepaintBoundary => true;
260

261
  /// Called during [layout] to determine the block's children.
262 263 264
  ///
  /// Typically the callback will mutate the child list appropriately, for
  /// example so the child list contains only visible children.
265
  LayoutCallback get callback => _callback;
266
  LayoutCallback _callback;
267 268 269 270 271 272 273 274
  void set callback(LayoutCallback value) {
    assert(!_inCallback);
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

275 276 277 278 279 280
  /// Called during after [layout].
  ///
  /// This callback cannot mutate the tree. To mutate the tree during
  /// layout, use [callback].
  VoidCallback postLayoutCallback;

Florian Loitsch's avatar
Florian Loitsch committed
281
  /// Returns the total main-axis extent of all the children that could be included by [callback] in one go.
282 283 284
  ExtentCallback get totalExtentCallback => _totalExtentCallback;
  ExtentCallback _totalExtentCallback;
  void set totalExtentCallback(ExtentCallback value) {
285 286 287 288 289 290 291
    assert(!_inCallback);
    if (value == _totalExtentCallback)
      return;
    _totalExtentCallback = value;
    markNeedsLayout();
  }

Florian Loitsch's avatar
Florian Loitsch committed
292
  /// Returns the minimum cross-axis extent across all the children that could be included by [callback] in one go.
293 294 295
  ExtentCallback get minCrossAxisExtentCallback => _minCrossAxisExtentCallback;
  ExtentCallback _minCrossAxisExtentCallback;
  void set minCrossAxisExtentCallback(ExtentCallback value) {
296
    assert(!_inCallback);
297
    if (value == _minCrossAxisExtentCallback)
298
      return;
299
    _minCrossAxisExtentCallback = value;
300 301 302
    markNeedsLayout();
  }

Florian Loitsch's avatar
Florian Loitsch committed
303
  /// Returns the maximum cross-axis extent across all the children that could be included by [callback] in one go.
304 305 306
  ExtentCallback get maxCrossAxisExtentCallback => _maxCrossAxisExtentCallback;
  ExtentCallback _maxCrossAxisExtentCallback;
  void set maxCrossAxisExtentCallback(ExtentCallback value) {
307
    assert(!_inCallback);
308
    if (value == _maxCrossAxisExtentCallback)
309
      return;
310
    _maxCrossAxisExtentCallback = value;
311 312 313
    markNeedsLayout();
  }

314 315 316 317 318
  Painter get overlayPainter => _overlayPainter;
  Painter _overlayPainter;
  void set overlayPainter(Painter value) {
    if (_overlayPainter == value)
      return;
319 320
    if (attached)
      _overlayPainter?.detach();
321
    _overlayPainter = value;
322 323
    if (attached)
      _overlayPainter?.attach(this);
324 325 326 327 328 329 330 331 332 333 334 335 336
    markNeedsPaint();
  }

  void attach() {
    super.attach();
    _overlayPainter?.attach(this);
  }

  void detach() {
    super.detach();
    _overlayPainter?.detach();
  }

Florian Loitsch's avatar
Florian Loitsch committed
337
  /// The offset at which to paint the first child.
338 339
  ///
  /// Note: you can modify this property from within [callback], if necessary.
340
  double get startOffset => _startOffset;
341
  double _startOffset;
342
  void set startOffset(double value) {
343 344
    if (value != _startOffset) {
      _startOffset = value;
345
      markNeedsPaint();
Hixie's avatar
Hixie committed
346
      markNeedsSemanticsUpdate();
347
    }
348 349
  }

350
  double _getIntrinsicDimension(BoxConstraints constraints, ExtentCallback intrinsicCallback, _Constrainer constrainer) {
351 352 353 354
    assert(!_inCallback);
    double result;
    if (intrinsicCallback == null) {
      assert(() {
355 356 357
        if (!RenderObject.debugCheckingIntrinsics)
          throw new UnsupportedError('$runtimeType does not support returning intrinsic dimensions if the relevant callbacks have not been specified.');
        return true;
358 359 360 361 362 363
      });
      return constrainer(0.0);
    }
    try {
      _inCallback = true;
      result = intrinsicCallback(constraints);
Florian Loitsch's avatar
Florian Loitsch committed
364
      result = constrainer(result ?? 0.0);
365 366 367 368
    } finally {
      _inCallback = false;
    }
    return result;
369 370
  }

371
  double getMinIntrinsicWidth(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
372
    assert(constraints.debugAssertIsNormalized);
373
    if (isVertical)
374
      return _getIntrinsicDimension(constraints, minCrossAxisExtentCallback, constraints.constrainWidth);
375
    return constraints.constrainWidth(minExtent);
376 377 378
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
379
    assert(constraints.debugAssertIsNormalized);
380
    if (isVertical)
381
      return _getIntrinsicDimension(constraints, maxCrossAxisExtentCallback, constraints.constrainWidth);
382
    return _getIntrinsicDimension(constraints, totalExtentCallback, new BoxConstraints(minWidth: minExtent).enforce(constraints).constrainWidth);
383 384 385
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
386
    assert(constraints.debugAssertIsNormalized);
387
    if (!isVertical)
388
      return _getIntrinsicDimension(constraints, minCrossAxisExtentCallback, constraints.constrainHeight);
389
    return constraints.constrainHeight(0.0);
390 391 392
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
Hixie's avatar
Hixie committed
393
    assert(constraints.debugAssertIsNormalized);
394
    if (!isVertical)
395
      return _getIntrinsicDimension(constraints, maxCrossAxisExtentCallback, constraints.constrainHeight);
396
    return _getIntrinsicDimension(constraints, totalExtentCallback, new BoxConstraints(minHeight: minExtent).enforce(constraints).constrainHeight);
397 398 399
  }

  // We don't override computeDistanceToActualBaseline(), because we
400
  // want the default behavior (returning null). Otherwise, as you
401 402 403 404 405 406 407 408 409 410 411 412 413
  // scroll the RenderBlockViewport, it would shift in its parent if
  // the parent was baseline-aligned, which makes no sense.

  void performLayout() {
    if (_callback != null) {
      try {
        _inCallback = true;
        invokeLayoutCallback(_callback);
      } finally {
        _inCallback = false;
      }
    }
    super.performLayout();
414 415
    if (postLayoutCallback != null)
      postLayoutCallback();
416 417
  }

418
  void _paintContents(PaintingContext context, Offset offset) {
419 420 421 422
    if (isVertical)
      defaultPaint(context, offset.translate(0.0, startOffset));
    else
      defaultPaint(context, offset.translate(startOffset, 0.0));
423 424

    overlayPainter?.paint(context, offset);
425
  }
426

427 428
  void paint(PaintingContext context, Offset offset) {
    context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
429 430
  }

431
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
432 433 434 435
    if (isVertical)
      transform.translate(0.0, startOffset);
    else
      transform.translate(startOffset, 0.0);
436
    super.applyPaintTransform(child, transform);
437 438
  }

Hixie's avatar
Hixie committed
439 440
  Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;

Adam Barth's avatar
Adam Barth committed
441
  bool hitTestChildren(HitTestResult result, { Point position }) {
442
    if (isVertical)
Adam Barth's avatar
Adam Barth committed
443
      return defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));
444
    else
Adam Barth's avatar
Adam Barth committed
445
      return defaultHitTestChildren(result, position: position + new Offset(-startOffset, 0.0));
446 447
  }

448 449 450
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('startOffset: $startOffset');
451
  }
452
}