block.dart 14.1 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 8
import 'package:sky/src/rendering/box.dart';
import 'package:sky/src/rendering/object.dart';
9
import 'package:vector_math/vector_math.dart';
10

11
/// Parent data for use with [RenderBlockBase]
12 13
class BlockParentData extends BoxParentData with ContainerParentDataMixin<RenderBox> { }

14 15 16 17 18 19 20
/// The direction in which the block should lay out
enum BlockDirection {
  /// Children are arranged horizontally, from left to right
  horizontal,
  /// Children are arranged vertically, from top to bottom
  vertical
}
21 22

typedef double _ChildSizingFunction(RenderBox child, BoxConstraints constraints);
23
typedef double _Constrainer(double value);
24

25 26 27 28 29 30 31 32 33
/// Implements the block layout algorithm
///
/// 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.
34 35 36 37
abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin<RenderBox, BlockParentData>,
                                                      RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData> {

  RenderBlockBase({
38
    List<RenderBox> children,
39 40 41 42
    BlockDirection direction: BlockDirection.vertical,
    double itemExtent,
    double minExtent: 0.0
  }) : _direction = direction, _itemExtent = itemExtent, _minExtent = minExtent {
43 44 45 46 47 48 49 50
    addAll(children);
  }

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

51
  /// The direction to use as the main axis
52
  BlockDirection get direction => _direction;
53
  BlockDirection _direction;
54 55 56 57 58 59 60
  void set direction (BlockDirection value) {
    if (_direction != value) {
      _direction = value;
      markNeedsLayout();
    }
  }

61
  /// If non-null, forces children to be exactly this large in the main axis
62
  double get itemExtent => _itemExtent;
63
  double _itemExtent;
64 65 66 67 68 69 70
  void set itemExtent(double value) {
    if (value != _itemExtent) {
      _itemExtent = value;
      markNeedsLayout();
    }
  }

71
  /// Forces the block to be at least this large in the main-axis
72
  double get minExtent => _minExtent;
73
  double _minExtent;
74 75 76 77 78 79 80
  void set minExtent(double value) {
    if (value != _minExtent) {
      _minExtent = value;
      markNeedsLayout();
    }
  }

81
  /// Whether the main axis is vertical
82
  bool get isVertical => _direction == BlockDirection.vertical;
83

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

92 93 94
  double get _mainAxisExtent {
    RenderBox child = lastChild;
    if (child == null)
95
      return minExtent;
96
    BoxParentData parentData = child.parentData;
97
    return isVertical ?
98 99
        math.max(minExtent, parentData.position.y + child.size.height) :
        math.max(minExtent, parentData.position.x + child.size.width);
100 101
  }

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

Hixie's avatar
Hixie committed
119
  String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}direction: ${direction}\n';
120 121
}

122
/// A block layout with a concrete set of children
123 124
class RenderBlock extends RenderBlockBase {

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

132 133
  double _getIntrinsicCrossAxis(BoxConstraints constraints, _ChildSizingFunction childSize) {
    double extent = 0.0;
134
    BoxConstraints innerConstraints = isVertical ? constraints.widthConstraints() : constraints.heightConstraints();
135 136
    RenderBox child = firstChild;
    while (child != null) {
137
      extent = math.max(extent, childSize(child, innerConstraints));
138 139 140
      assert(child.parentData is BlockParentData);
      child = child.parentData.nextSibling;
    }
141
    return extent;
142 143
  }

144 145 146
  double _getIntrinsicMainAxis(BoxConstraints constraints) {
    double extent = 0.0;
    BoxConstraints innerConstraints = _getInnerConstraints(constraints);
147 148
    RenderBox child = firstChild;
    while (child != null) {
149
      double childExtent = isVertical ?
150 151 152
        child.getMinIntrinsicHeight(innerConstraints) :
        child.getMinIntrinsicWidth(innerConstraints);
      assert(() {
153
        if (isVertical)
154 155 156 157
          return childExtent == child.getMaxIntrinsicHeight(innerConstraints);
        return childExtent == child.getMaxIntrinsicWidth(innerConstraints);
      });
      extent += childExtent;
158 159 160
      assert(child.parentData is BlockParentData);
      child = child.parentData.nextSibling;
    }
161
    return math.max(extent, minExtent);
162 163
  }

164
  double getMinIntrinsicWidth(BoxConstraints constraints) {
165
    if (isVertical) {
166 167 168 169 170 171 172
      return _getIntrinsicCrossAxis(constraints,
        (c, innerConstraints) => c.getMinIntrinsicWidth(innerConstraints));
    }
    return _getIntrinsicMainAxis(constraints);
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
173
    if (isVertical) {
174 175
      return _getIntrinsicCrossAxis(constraints,
          (c, innerConstraints) => c.getMaxIntrinsicWidth(innerConstraints));
176
    }
177
    return _getIntrinsicMainAxis(constraints);
178 179 180
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
181
    if (isVertical)
182 183 184
      return _getIntrinsicMainAxis(constraints);
    return _getIntrinsicCrossAxis(constraints,
        (c, innerConstraints) => c.getMinIntrinsicWidth(innerConstraints));
185 186 187
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
188
    if (isVertical)
189 190 191
      return _getIntrinsicMainAxis(constraints);
    return _getIntrinsicCrossAxis(constraints,
        (c, innerConstraints) => c.getMaxIntrinsicWidth(innerConstraints));
192 193 194 195 196 197 198
  }

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

  void performLayout() {
199
    assert((isVertical ? constraints.maxHeight >= double.INFINITY : constraints.maxWidth >= double.INFINITY) &&
200 201
           'RenderBlock does not clip or resize its children, so it must be placed in a parent that does not constrain ' +
           'the block\'s main direction. You probably want to put the RenderBlock inside a RenderViewport.' is String);
202 203 204
    super.performLayout();
  }

205 206
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
207 208 209 210 211 212 213 214
  }

  void hitTestChildren(HitTestResult result, { Point position }) {
    defaultHitTestChildren(result, position: position);
  }

}

215 216 217 218 219 220 221 222
/// A block layout whose children depend on its layout
///
/// 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.
223 224 225 226
class RenderBlockViewport extends RenderBlockBase {

  RenderBlockViewport({
    LayoutCallback callback,
227 228 229
    ExtentCallback totalExtentCallback,
    ExtentCallback maxCrossAxisDimensionCallback,
    ExtentCallback minCrossAxisDimensionCallback,
230 231 232
    BlockDirection direction: BlockDirection.vertical,
    double itemExtent,
    double minExtent: 0.0,
233
    double startOffset: 0.0,
234
    List<RenderBox> children
235 236
  }) : _callback = callback,
       _totalExtentCallback = totalExtentCallback,
237 238
       _maxCrossAxisExtentCallback = maxCrossAxisDimensionCallback,
       _minCrossAxisExtentCallback = minCrossAxisDimensionCallback,
239
       _startOffset = startOffset,
240
       super(children: children, direction: direction, itemExtent: itemExtent, minExtent: minExtent);
241 242 243

  bool _inCallback = false;

244 245 246 247
  /// Called during [layout] to determine the blocks children
  ///
  /// Typically the callback will mutate the child list appropriately, for
  /// example so the child list contains only visible children.
248
  LayoutCallback get callback => _callback;
249
  LayoutCallback _callback;
250 251 252 253 254 255 256 257
  void set callback(LayoutCallback value) {
    assert(!_inCallback);
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

258 259 260 261
  /// Returns the total main-axis extent of all the children that could be included by [callback] in one go
  ExtentCallback get totalExtentCallback => _totalExtentCallback;
  ExtentCallback _totalExtentCallback;
  void set totalExtentCallback(ExtentCallback value) {
262 263 264 265 266 267 268
    assert(!_inCallback);
    if (value == _totalExtentCallback)
      return;
    _totalExtentCallback = value;
    markNeedsLayout();
  }

269 270 271 272
  /// Returns the minimum cross-axis extent across all the children that could be included by [callback] in one go
  ExtentCallback get minCrossAxisExtentCallback => _minCrossAxisExtentCallback;
  ExtentCallback _minCrossAxisExtentCallback;
  void set minCrossAxisExtentCallback(ExtentCallback value) {
273
    assert(!_inCallback);
274
    if (value == _minCrossAxisExtentCallback)
275
      return;
276
    _minCrossAxisExtentCallback = value;
277 278 279
    markNeedsLayout();
  }

280 281 282 283
  /// Returns the maximum cross-axis extent across all the children that could be included by [callback] in one go
  ExtentCallback get maxCrossAxisExtentCallback => _maxCrossAxisExtentCallback;
  ExtentCallback _maxCrossAxisExtentCallback;
  void set maxCrossAxisExtentCallback(ExtentCallback value) {
284
    assert(!_inCallback);
285
    if (value == _maxCrossAxisExtentCallback)
286
      return;
287
    _maxCrossAxisExtentCallback = value;
288 289 290
    markNeedsLayout();
  }

291 292 293
  /// The offset at which to paint the first child
  ///
  /// Note: you can modify this property from within [callback], if necessary.
294
  double get startOffset => _startOffset;
295
  double _startOffset;
296
  void set startOffset(double value) {
297 298
    if (value != _startOffset) {
      _startOffset = value;
299
      markNeedsPaint();
300
    }
301 302
  }

303
  double _getIntrinsicDimension(BoxConstraints constraints, ExtentCallback intrinsicCallback, _Constrainer constrainer) {
304 305 306 307 308 309 310 311 312 313 314 315 316 317
    assert(!_inCallback);
    double result;
    if (intrinsicCallback == null) {
      assert(() {
        'RenderBlockViewport does not support returning intrinsic dimensions if the relevant callbacks have not been specified.';
        return false;
      });
      return constrainer(0.0);
    }
    try {
      _inCallback = true;
      result = intrinsicCallback(constraints);
      if (result == null)
        result = constrainer(0.0);
Hixie's avatar
Hixie committed
318 319
      else
        result = constrainer(result);
320 321 322 323
    } finally {
      _inCallback = false;
    }
    return result;
324 325
  }

326
  double getMinIntrinsicWidth(BoxConstraints constraints) {
327
    if (isVertical)
328
      return _getIntrinsicDimension(constraints, minCrossAxisExtentCallback, constraints.constrainWidth);
329
    return constraints.constrainWidth(minExtent);
330 331 332
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
333
    if (isVertical)
334
      return _getIntrinsicDimension(constraints, maxCrossAxisExtentCallback, constraints.constrainWidth);
335
    return _getIntrinsicDimension(constraints, totalExtentCallback, new BoxConstraints(minWidth: minExtent).enforce(constraints).constrainWidth);
336 337 338
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
339
    if (!isVertical)
340
      return _getIntrinsicDimension(constraints, minCrossAxisExtentCallback, constraints.constrainHeight);
341
    return constraints.constrainHeight(0.0);
342 343 344
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
345
    if (!isVertical)
346
      return _getIntrinsicDimension(constraints, maxCrossAxisExtentCallback, constraints.constrainHeight);
347
    return _getIntrinsicDimension(constraints, totalExtentCallback, new BoxConstraints(minHeight: minExtent).enforce(constraints).constrainHeight);
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
  }

  // We don't override computeDistanceToActualBaseline(), because we
  // want the default behaviour (returning null). Otherwise, as you
  // scroll the RenderBlockViewport, it would shift in its parent if
  // the parent was baseline-aligned, which makes no sense.

  bool get debugDoesLayoutWithCallback => true;
  void performLayout() {
    if (_callback != null) {
      try {
        _inCallback = true;
        invokeLayoutCallback(_callback);
      } finally {
        _inCallback = false;
      }
    }
    super.performLayout();
  }

368 369 370
  void paint(PaintingContext context, Offset offset) {
    context.canvas.save();
    context.canvas.clipRect(offset & size);
371 372 373 374
    if (isVertical)
      defaultPaint(context, offset.translate(0.0, startOffset));
    else
      defaultPaint(context, offset.translate(startOffset, 0.0));
375
    context.canvas.restore();
376 377
  }

378 379 380 381 382
  void applyPaintTransform(Matrix4 transform) {
    super.applyPaintTransform(transform);
    transform.translate(0.0, startOffset);
  }

383 384 385 386
  void hitTestChildren(HitTestResult result, { Point position }) {
    defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));
  }

Hixie's avatar
Hixie committed
387
  String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}startOffset: ${startOffset}\n';
388
}