blob: d56b23071eefde72fd8a0b251124cd7f815e3e14 [file] [log] [blame]
// 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:ui' as ui show window;
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'object.dart';
/// The end of the viewport from which the paint offset is computed.
enum ViewportAnchor {
/// The start (e.g., top or left, depending on the axis) of the first item
/// should be aligned with the start (e.g., top or left, depending on the
/// axis) of the viewport.
start,
/// The end (e.g., bottom or right, depending on the axis) of the last item
/// should be aligned with the end (e.g., bottom or right, depending on the
/// axis) of the viewport.
end,
}
/// The interior and exterior dimensions of a viewport.
class ViewportDimensions {
const ViewportDimensions({
this.contentSize: Size.zero,
this.containerSize: Size.zero
});
/// A viewport that has zero size, both inside and outside.
static const ViewportDimensions zero = const ViewportDimensions();
/// The size of the content inside the viewport.
final Size contentSize;
/// The size of the outside of the viewport.
final Size containerSize;
bool get _debugHasAtLeastOneCommonDimension {
return contentSize.width == containerSize.width
|| contentSize.height == containerSize.height;
}
/// Returns the offset at which to paint the content, accounting for the given
/// anchor and the dimensions of the viewport.
Offset getAbsolutePaintOffset({ Offset paintOffset, ViewportAnchor anchor }) {
assert(_debugHasAtLeastOneCommonDimension);
switch (anchor) {
case ViewportAnchor.start:
return paintOffset;
case ViewportAnchor.end:
return paintOffset + (containerSize - contentSize);
}
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! ViewportDimensions)
return false;
final ViewportDimensions typedOther = other;
return contentSize == typedOther.contentSize &&
containerSize == typedOther.containerSize;
}
@override
int get hashCode => hashValues(contentSize, containerSize);
@override
String toString() => 'ViewportDimensions(container: $containerSize, content: $contentSize)';
}
/// An interface that indicates that an object has a scroll direction.
abstract class HasMainAxis {
/// Whether this object scrolls horizontally or vertically.
Axis get mainAxis;
}
/// A base class for render objects that are bigger on the inside.
///
/// This class holds the common fields for viewport render objects but does not
/// have a child model. See [RenderViewport] for a viewport with a single child
/// and [RenderVirtualViewport] for a viewport with multiple children.
class RenderViewportBase extends RenderBox implements HasMainAxis {
RenderViewportBase(
Offset paintOffset,
Axis mainAxis,
ViewportAnchor anchor,
RenderObjectPainter overlayPainter
) : _paintOffset = paintOffset,
_mainAxis = mainAxis,
_anchor = anchor,
_overlayPainter = overlayPainter {
assert(paintOffset != null);
assert(mainAxis != null);
assert(_offsetIsSane(_paintOffset, mainAxis));
}
bool _offsetIsSane(Offset offset, Axis direction) {
switch (direction) {
case Axis.horizontal:
return offset.dy == 0.0;
case Axis.vertical:
return offset.dx == 0.0;
}
}
/// The offset at which to paint the child.
///
/// The offset can be non-zero only in the [mainAxis].
Offset get paintOffset => _paintOffset;
Offset _paintOffset;
void set paintOffset(Offset value) {
assert(value != null);
if (value == _paintOffset)
return;
assert(_offsetIsSane(value, mainAxis));
_paintOffset = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The direction in which the child is permitted to be larger than the viewport
///
/// If the viewport is scrollable in a particular direction (e.g., vertically),
/// the child is given layout constraints that are fully unconstrainted in
/// that direction (e.g., the child can be as tall as it wants).
@override
Axis get mainAxis => _mainAxis;
Axis _mainAxis;
void set mainAxis(Axis value) {
assert(value != null);
if (value == _mainAxis)
return;
assert(_offsetIsSane(_paintOffset, value));
_mainAxis = value;
markNeedsLayout();
}
/// The end of the viewport from which the paint offset is computed.
///
/// See [ViewportAnchor] for more detail.
ViewportAnchor get anchor => _anchor;
ViewportAnchor _anchor;
void set anchor(ViewportAnchor value) {
assert(value != null);
if (value == _anchor)
return;
_anchor = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
RenderObjectPainter get overlayPainter => _overlayPainter;
RenderObjectPainter _overlayPainter;
void set overlayPainter(RenderObjectPainter value) {
if (_overlayPainter == value)
return;
if (attached)
_overlayPainter?.detach();
_overlayPainter = value;
if (attached)
_overlayPainter?.attach(this);
markNeedsPaint();
}
@override
void attach() {
super.attach();
_overlayPainter?.attach(this);
}
@override
void detach() {
super.detach();
_overlayPainter?.detach();
}
ViewportDimensions get dimensions => _dimensions;
ViewportDimensions _dimensions = ViewportDimensions.zero;
void set dimensions(ViewportDimensions value) {
assert(debugDoingThisLayout);
_dimensions = value;
}
Offset get _effectivePaintOffset {
final double devicePixelRatio = ui.window.devicePixelRatio;
int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round();
int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round();
return _dimensions.getAbsolutePaintOffset(
paintOffset: new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio),
anchor: _anchor
);
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final Offset effectivePaintOffset = _effectivePaintOffset;
super.applyPaintTransform(child, transform.translate(effectivePaintOffset.dx, effectivePaintOffset.dy));
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('paintOffset: $paintOffset');
description.add('mainAxis: $mainAxis');
description.add('anchor: $anchor');
if (overlayPainter != null)
description.add('overlay painter: $overlayPainter');
}
}
typedef Offset ViewportDimensionsChangeCallback(ViewportDimensions dimensions);
/// A render object that's bigger on the inside.
///
/// The child of a viewport can layout to a larger size than the viewport
/// itself. If that happens, only a portion of the child will be visible through
/// the viewport. The portion of the child that is visible is controlled by the
/// paint offset.
class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<RenderBox> {
RenderViewport({
RenderBox child,
Offset paintOffset: Offset.zero,
Axis mainAxis: Axis.vertical,
ViewportAnchor anchor: ViewportAnchor.start,
RenderObjectPainter overlayPainter,
this.onPaintOffsetUpdateNeeded
}) : super(paintOffset, mainAxis, anchor, overlayPainter) {
this.child = child;
}
/// Called during [layout] to report the dimensions of the viewport
/// and its child.
ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
BoxConstraints innerConstraints;
switch (mainAxis) {
case Axis.horizontal:
innerConstraints = constraints.heightConstraints();
break;
case Axis.vertical:
innerConstraints = constraints.widthConstraints();
break;
}
return innerConstraints;
}
@override
double getMinIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return constraints.constrainWidth(child.getMinIntrinsicWidth(_getInnerConstraints(constraints)));
return super.getMinIntrinsicWidth(constraints);
}
@override
double getMaxIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return constraints.constrainWidth(child.getMaxIntrinsicWidth(_getInnerConstraints(constraints)));
return super.getMaxIntrinsicWidth(constraints);
}
@override
double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return constraints.constrainHeight(child.getMinIntrinsicHeight(_getInnerConstraints(constraints)));
return super.getMinIntrinsicHeight(constraints);
}
@override
double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return constraints.constrainHeight(child.getMaxIntrinsicHeight(_getInnerConstraints(constraints)));
return super.getMaxIntrinsicHeight(constraints);
}
// We don't override computeDistanceToActualBaseline(), because we
// want the default behavior (returning null). Otherwise, as you
// scroll the RenderViewport, it would shift in its parent if the
// parent was baseline-aligned, which makes no sense.
@override
void performLayout() {
ViewportDimensions oldDimensions = dimensions;
if (child != null) {
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
size = constraints.constrain(child.size);
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset.zero;
dimensions = new ViewportDimensions(containerSize: size, contentSize: child.size);
} else {
performResize();
dimensions = new ViewportDimensions(containerSize: size);
}
if (onPaintOffsetUpdateNeeded != null && dimensions != oldDimensions)
paintOffset = onPaintOffsetUpdateNeeded(dimensions);
assert(paintOffset != null);
}
bool _shouldClipAtPaintOffset(Offset paintOffset) {
assert(child != null);
return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Offset effectivePaintOffset = _effectivePaintOffset;
void paintContents(PaintingContext context, Offset offset) {
context.paintChild(child, offset + effectivePaintOffset);
_overlayPainter?.paint(context, offset);
}
if (_shouldClipAtPaintOffset(effectivePaintOffset)) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
} else {
paintContents(context, offset);
}
}
}
@override
Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && _shouldClipAtPaintOffset(_effectivePaintOffset))
return Point.origin & size;
return null;
}
@override
bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) {
assert(child.parentData is BoxParentData);
Point transformed = position + -_effectivePaintOffset;
return child.hitTest(result, position: transformed);
}
return false;
}
}
abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<RenderBox>>
extends RenderViewportBase with ContainerRenderObjectMixin<RenderBox, T>,
RenderBoxContainerDefaultsMixin<RenderBox, T> {
RenderVirtualViewport({
int virtualChildCount,
LayoutCallback callback,
Offset paintOffset: Offset.zero,
Axis mainAxis: Axis.vertical,
ViewportAnchor anchor: ViewportAnchor.start,
RenderObjectPainter overlayPainter
}) : _virtualChildCount = virtualChildCount,
_callback = callback,
super(paintOffset, mainAxis, anchor, overlayPainter);
int get virtualChildCount => _virtualChildCount;
int _virtualChildCount;
void set virtualChildCount(int value) {
if (_virtualChildCount == value)
return;
_virtualChildCount = value;
markNeedsLayout();
}
/// Called during [layout] to determine the render object's children.
///
/// Typically the callback will mutate the child list appropriately, for
/// example so the child list contains only visible children.
LayoutCallback get callback => _callback;
LayoutCallback _callback;
void set callback(LayoutCallback value) {
if (value == _callback)
return;
_callback = value;
markNeedsLayout();
}
@override
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position + -_effectivePaintOffset);
}
void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + _effectivePaintOffset);
_overlayPainter?.paint(context, offset);
}
@override
void paint(PaintingContext context, Offset offset) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
}
@override
Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('virtual child count: $virtualChildCount');
}
}