| // 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; |
| |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/gestures.dart'; |
| |
| const double kTwoPi = 2 * math.PI; |
| |
| class SectorConstraints extends Constraints { |
| const SectorConstraints({ |
| this.minDeltaRadius: 0.0, |
| this.maxDeltaRadius: double.INFINITY, |
| this.minDeltaTheta: 0.0, |
| this.maxDeltaTheta: kTwoPi |
| }); |
| |
| const SectorConstraints.tight({ double deltaRadius: 0.0, double deltaTheta: 0.0 }) |
| : minDeltaRadius = deltaRadius, |
| maxDeltaRadius = deltaRadius, |
| minDeltaTheta = deltaTheta, |
| maxDeltaTheta = deltaTheta; |
| |
| final double minDeltaRadius; |
| final double maxDeltaRadius; |
| final double minDeltaTheta; |
| final double maxDeltaTheta; |
| |
| double constrainDeltaRadius(double deltaRadius) { |
| return clamp(min: minDeltaRadius, max: maxDeltaRadius, value: deltaRadius); |
| } |
| |
| double constrainDeltaTheta(double deltaTheta) { |
| return clamp(min: minDeltaTheta, max: maxDeltaTheta, value: deltaTheta); |
| } |
| |
| bool get isTight => minDeltaTheta >= maxDeltaTheta && minDeltaTheta >= maxDeltaTheta; |
| } |
| |
| class SectorDimensions { |
| const SectorDimensions({ this.deltaRadius: 0.0, this.deltaTheta: 0.0 }); |
| |
| factory SectorDimensions.withConstraints( |
| SectorConstraints constraints, |
| { double deltaRadius: 0.0, double deltaTheta: 0.0 } |
| ) { |
| return new SectorDimensions( |
| deltaRadius: constraints.constrainDeltaRadius(deltaRadius), |
| deltaTheta: constraints.constrainDeltaTheta(deltaTheta) |
| ); |
| } |
| |
| final double deltaRadius; |
| final double deltaTheta; |
| } |
| |
| class SectorParentData extends ParentData { |
| double radius = 0.0; |
| double theta = 0.0; |
| } |
| |
| abstract class RenderSector extends RenderObject { |
| |
| void setupParentData(RenderObject child) { |
| if (child.parentData is! SectorParentData) |
| child.parentData = new SectorParentData(); |
| } |
| |
| // RenderSectors always use SectorParentData subclasses, as they need to be |
| // able to read their position information for painting and hit testing. |
| SectorParentData get parentData => super.parentData; |
| |
| SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { |
| return new SectorDimensions.withConstraints(constraints); |
| } |
| |
| SectorConstraints get constraints => super.constraints; |
| bool debugDoesMeetConstraints() { |
| assert(constraints != null); |
| assert(deltaRadius != null); |
| assert(deltaRadius < double.INFINITY); |
| assert(deltaTheta != null); |
| assert(deltaTheta < double.INFINITY); |
| return constraints.minDeltaRadius <= deltaRadius && |
| deltaRadius <= math.max(constraints.minDeltaRadius, constraints.maxDeltaRadius) && |
| constraints.minDeltaTheta <= deltaTheta && |
| deltaTheta <= math.max(constraints.minDeltaTheta, constraints.maxDeltaTheta); |
| } |
| void performResize() { |
| // default behaviour for subclasses that have sizedByParent = true |
| deltaRadius = constraints.constrainDeltaRadius(0.0); |
| deltaTheta = constraints.constrainDeltaTheta(0.0); |
| } |
| void performLayout() { |
| // descendants have to either override performLayout() to set both |
| // the dimensions and lay out children, or, set sizedByParent to |
| // true so that performResize()'s logic above does its thing. |
| assert(sizedByParent); |
| } |
| |
| Rect get paintBounds => new Rect.fromLTWH(0.0, 0.0, 2.0 * deltaRadius, 2.0 * deltaRadius); |
| |
| bool hitTest(HitTestResult result, { double radius, double theta }) { |
| if (radius < parentData.radius || radius >= parentData.radius + deltaRadius || |
| theta < parentData.theta || theta >= parentData.theta + deltaTheta) |
| return false; |
| hitTestChildren(result, radius: radius, theta: theta); |
| result.add(new HitTestEntry(this)); |
| return true; |
| } |
| void hitTestChildren(HitTestResult result, { double radius, double theta }) { } |
| |
| double deltaRadius; |
| double deltaTheta; |
| } |
| |
| abstract class RenderDecoratedSector extends RenderSector { |
| |
| RenderDecoratedSector(BoxDecoration decoration) : _decoration = decoration; |
| |
| BoxDecoration _decoration; |
| BoxDecoration get decoration => _decoration; |
| void set decoration (BoxDecoration value) { |
| if (value == _decoration) |
| return; |
| _decoration = value; |
| markNeedsPaint(); |
| } |
| |
| // offset must point to the center of the circle |
| void paint(PaintingContext context, Offset offset) { |
| assert(deltaRadius != null); |
| assert(deltaTheta != null); |
| assert(parentData is SectorParentData); |
| |
| if (_decoration == null) |
| return; |
| |
| if (_decoration.backgroundColor != null) { |
| final Canvas canvas = context.canvas; |
| Paint paint = new Paint()..color = _decoration.backgroundColor; |
| Path path = new Path(); |
| double outerRadius = (parentData.radius + deltaRadius); |
| Rect outerBounds = new Rect.fromLTRB(offset.dx-outerRadius, offset.dy-outerRadius, offset.dx+outerRadius, offset.dy+outerRadius); |
| path.arcTo(outerBounds, parentData.theta, deltaTheta, true); |
| double innerRadius = parentData.radius; |
| Rect innerBounds = new Rect.fromLTRB(offset.dx-innerRadius, offset.dy-innerRadius, offset.dx+innerRadius, offset.dy+innerRadius); |
| path.arcTo(innerBounds, parentData.theta + deltaTheta, -deltaTheta, false); |
| path.close(); |
| canvas.drawPath(path, paint); |
| } |
| } |
| |
| } |
| |
| class SectorChildListParentData extends SectorParentData with ContainerParentDataMixin<RenderSector> { } |
| |
| class RenderSectorWithChildren extends RenderDecoratedSector with ContainerRenderObjectMixin<RenderSector, SectorChildListParentData> { |
| RenderSectorWithChildren(BoxDecoration decoration) : super(decoration); |
| |
| void hitTestChildren(HitTestResult result, { double radius, double theta }) { |
| RenderSector child = lastChild; |
| while (child != null) { |
| if (child.hitTest(result, radius: radius, theta: theta)) |
| return; |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.previousSibling; |
| } |
| } |
| |
| void visitChildren(RenderObjectVisitor visitor) { |
| RenderSector child = lastChild; |
| while (child != null) { |
| visitor(child); |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.previousSibling; |
| } |
| } |
| } |
| |
| class RenderSectorRing extends RenderSectorWithChildren { |
| // lays out RenderSector children in a ring |
| |
| RenderSectorRing({ |
| BoxDecoration decoration, |
| double deltaRadius: double.INFINITY, |
| double padding: 0.0 |
| }) : _padding = padding, _desiredDeltaRadius = deltaRadius, super(decoration); |
| |
| double _desiredDeltaRadius; |
| double get desiredDeltaRadius => _desiredDeltaRadius; |
| void set desiredDeltaRadius(double value) { |
| assert(value != null); |
| if (_desiredDeltaRadius != value) { |
| _desiredDeltaRadius = value; |
| markNeedsLayout(); |
| } |
| } |
| |
| double _padding; |
| double get padding => _padding; |
| void set padding(double value) { |
| // TODO(ianh): avoid code duplication |
| assert(value != null); |
| if (_padding != value) { |
| _padding = value; |
| markNeedsLayout(); |
| } |
| } |
| |
| void setupParentData(RenderObject child) { |
| // TODO(ianh): avoid code duplication |
| if (child.parentData is! SectorChildListParentData) |
| child.parentData = new SectorChildListParentData(); |
| } |
| |
| SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { |
| double outerDeltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius); |
| double innerDeltaRadius = outerDeltaRadius - padding * 2.0; |
| double childRadius = radius + padding; |
| double paddingTheta = math.atan(padding / (radius + outerDeltaRadius)); |
| double innerTheta = paddingTheta; // increments with each child |
| double remainingDeltaTheta = constraints.maxDeltaTheta - (innerTheta + paddingTheta); |
| RenderSector child = firstChild; |
| while (child != null) { |
| SectorConstraints innerConstraints = new SectorConstraints( |
| maxDeltaRadius: innerDeltaRadius, |
| maxDeltaTheta: remainingDeltaTheta |
| ); |
| SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius); |
| innerTheta += childDimensions.deltaTheta; |
| remainingDeltaTheta -= childDimensions.deltaTheta; |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.nextSibling; |
| if (child != null) { |
| innerTheta += paddingTheta; |
| remainingDeltaTheta -= paddingTheta; |
| } |
| } |
| return new SectorDimensions.withConstraints(constraints, |
| deltaRadius: outerDeltaRadius, |
| deltaTheta: innerTheta); |
| } |
| |
| void performLayout() { |
| assert(this.parentData is SectorParentData); |
| deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius); |
| assert(deltaRadius < double.INFINITY); |
| double innerDeltaRadius = deltaRadius - padding * 2.0; |
| double childRadius = this.parentData.radius + padding; |
| double paddingTheta = math.atan(padding / (this.parentData.radius + deltaRadius)); |
| double innerTheta = paddingTheta; // increments with each child |
| double remainingDeltaTheta = constraints.maxDeltaTheta - (innerTheta + paddingTheta); |
| RenderSector child = firstChild; |
| while (child != null) { |
| SectorConstraints innerConstraints = new SectorConstraints( |
| maxDeltaRadius: innerDeltaRadius, |
| maxDeltaTheta: remainingDeltaTheta |
| ); |
| assert(child.parentData is SectorParentData); |
| child.parentData.theta = innerTheta; |
| child.parentData.radius = childRadius; |
| child.layout(innerConstraints, parentUsesSize: true); |
| innerTheta += child.deltaTheta; |
| remainingDeltaTheta -= child.deltaTheta; |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.nextSibling; |
| if (child != null) { |
| innerTheta += paddingTheta; |
| remainingDeltaTheta -= paddingTheta; |
| } |
| } |
| deltaTheta = innerTheta; |
| } |
| |
| // offset must point to the center of our circle |
| // each sector then knows how to paint itself at its location |
| void paint(PaintingContext context, Offset offset) { |
| // TODO(ianh): avoid code duplication |
| super.paint(context, offset); |
| RenderSector child = firstChild; |
| while (child != null) { |
| context.paintChild(child, offset); |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.nextSibling; |
| } |
| } |
| |
| } |
| |
| class RenderSectorSlice extends RenderSectorWithChildren { |
| // lays out RenderSector children in a stack |
| |
| RenderSectorSlice({ |
| BoxDecoration decoration, |
| double deltaTheta: kTwoPi, |
| double padding: 0.0 |
| }) : _padding = padding, _desiredDeltaTheta = deltaTheta, super(decoration); |
| |
| double _desiredDeltaTheta; |
| double get desiredDeltaTheta => _desiredDeltaTheta; |
| void set desiredDeltaTheta(double value) { |
| assert(value != null); |
| if (_desiredDeltaTheta != value) { |
| _desiredDeltaTheta = value; |
| markNeedsLayout(); |
| } |
| } |
| |
| double _padding; |
| double get padding => _padding; |
| void set padding(double value) { |
| // TODO(ianh): avoid code duplication |
| assert(value != null); |
| if (_padding != value) { |
| _padding = value; |
| markNeedsLayout(); |
| } |
| } |
| |
| void setupParentData(RenderObject child) { |
| // TODO(ianh): avoid code duplication |
| if (child.parentData is! SectorChildListParentData) |
| child.parentData = new SectorChildListParentData(); |
| } |
| |
| SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { |
| assert(this.parentData is SectorParentData); |
| double paddingTheta = math.atan(padding / this.parentData.radius); |
| double outerDeltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta); |
| double innerDeltaTheta = outerDeltaTheta - paddingTheta * 2.0; |
| double childRadius = this.parentData.radius + padding; |
| double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0); |
| RenderSector child = firstChild; |
| while (child != null) { |
| SectorConstraints innerConstraints = new SectorConstraints( |
| maxDeltaRadius: remainingDeltaRadius, |
| maxDeltaTheta: innerDeltaTheta |
| ); |
| SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius); |
| childRadius += childDimensions.deltaRadius; |
| remainingDeltaRadius -= childDimensions.deltaRadius; |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.nextSibling; |
| childRadius += padding; |
| remainingDeltaRadius -= padding; |
| } |
| return new SectorDimensions.withConstraints(constraints, |
| deltaRadius: childRadius - this.parentData.radius, |
| deltaTheta: outerDeltaTheta); |
| } |
| |
| void performLayout() { |
| assert(this.parentData is SectorParentData); |
| deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta); |
| assert(deltaTheta <= kTwoPi); |
| double paddingTheta = math.atan(padding / this.parentData.radius); |
| double innerTheta = this.parentData.theta + paddingTheta; |
| double innerDeltaTheta = deltaTheta - paddingTheta * 2.0; |
| double childRadius = this.parentData.radius + padding; |
| double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0); |
| RenderSector child = firstChild; |
| while (child != null) { |
| SectorConstraints innerConstraints = new SectorConstraints( |
| maxDeltaRadius: remainingDeltaRadius, |
| maxDeltaTheta: innerDeltaTheta |
| ); |
| child.parentData.theta = innerTheta; |
| child.parentData.radius = childRadius; |
| child.layout(innerConstraints, parentUsesSize: true); |
| childRadius += child.deltaRadius; |
| remainingDeltaRadius -= child.deltaRadius; |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.nextSibling; |
| childRadius += padding; |
| remainingDeltaRadius -= padding; |
| } |
| deltaRadius = childRadius - this.parentData.radius; |
| } |
| |
| // offset must point to the center of our circle |
| // each sector then knows how to paint itself at its location |
| void paint(PaintingContext context, Offset offset) { |
| // TODO(ianh): avoid code duplication |
| super.paint(context, offset); |
| RenderSector child = firstChild; |
| while (child != null) { |
| assert(child.parentData is SectorChildListParentData); |
| context.paintChild(child, offset); |
| final SectorChildListParentData childParentData = child.parentData; |
| child = childParentData.nextSibling; |
| } |
| } |
| |
| } |
| |
| class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChildMixin<RenderSector> { |
| |
| RenderBoxToRenderSectorAdapter({ double innerRadius: 0.0, RenderSector child }) : |
| _innerRadius = innerRadius { |
| this.child = child; |
| } |
| |
| double _innerRadius; |
| double get innerRadius => _innerRadius; |
| void set innerRadius(double value) { |
| _innerRadius = value; |
| markNeedsLayout(); |
| } |
| |
| void setupParentData(RenderObject child) { |
| if (child.parentData is! SectorParentData) |
| child.parentData = new SectorParentData(); |
| } |
| |
| double getMinIntrinsicWidth(BoxConstraints constraints) { |
| if (child == null) |
| return super.getMinIntrinsicWidth(constraints); |
| return getIntrinsicDimensions(constraints).width; |
| } |
| |
| double getMaxIntrinsicWidth(BoxConstraints constraints) { |
| if (child == null) |
| return super.getMaxIntrinsicWidth(constraints); |
| return getIntrinsicDimensions(constraints).width; |
| } |
| |
| double getMinIntrinsicHeight(BoxConstraints constraints) { |
| if (child == null) |
| return super.getMinIntrinsicHeight(constraints); |
| return getIntrinsicDimensions(constraints).height; |
| } |
| |
| double getMaxIntrinsicHeight(BoxConstraints constraints) { |
| if (child == null) |
| return super.getMaxIntrinsicHeight(constraints); |
| return getIntrinsicDimensions(constraints).height; |
| } |
| |
| Size getIntrinsicDimensions(BoxConstraints constraints) { |
| assert(child is RenderSector); |
| assert(child.parentData is SectorParentData); |
| assert(constraints.maxWidth < double.INFINITY || constraints.maxHeight < double.INFINITY); |
| double maxChildDeltaRadius = math.min(constraints.maxWidth, constraints.maxHeight) / 2.0 - innerRadius; |
| SectorDimensions childDimensions = child.getIntrinsicDimensions(new SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), innerRadius); |
| double dimension = (innerRadius + childDimensions.deltaRadius) * 2.0; |
| return constraints.constrain(new Size(dimension, dimension)); |
| } |
| |
| void performLayout() { |
| if (child == null) { |
| size = constraints.constrain(Size.zero); |
| } else { |
| assert(child is RenderSector); |
| assert(constraints.maxWidth < double.INFINITY || constraints.maxHeight < double.INFINITY); |
| double maxChildDeltaRadius = math.min(constraints.maxWidth, constraints.maxHeight) / 2.0 - innerRadius; |
| assert(child.parentData is SectorParentData); |
| child.parentData.radius = innerRadius; |
| child.parentData.theta = 0.0; |
| child.layout(new SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), parentUsesSize: true); |
| double dimension = (innerRadius + child.deltaRadius) * 2.0; |
| size = constraints.constrain(new Size(dimension, dimension)); |
| } |
| } |
| |
| void paint(PaintingContext context, Offset offset) { |
| super.paint(context, offset); |
| if (child != null) { |
| Rect bounds = offset & size; |
| // we move the offset to the center of the circle for the RenderSectors |
| context.paintChild(child, bounds.center.toOffset()); |
| } |
| } |
| |
| bool hitTest(HitTestResult result, { Point position }) { |
| if (child == null) |
| return false; |
| double x = position.x; |
| double y = position.y; |
| // translate to our origin |
| x -= size.width/2.0; |
| y -= size.height/2.0; |
| // convert to radius/theta |
| double radius = math.sqrt(x*x+y*y); |
| double theta = (math.atan2(x, -y) - math.PI/2.0) % kTwoPi; |
| if (radius < innerRadius) |
| return false; |
| if (radius >= innerRadius + child.deltaRadius) |
| return false; |
| if (theta > child.deltaTheta) |
| return false; |
| child.hitTest(result, radius: radius, theta: theta); |
| result.add(new BoxHitTestEntry(this, position)); |
| return true; |
| } |
| |
| } |
| |
| class RenderSolidColor extends RenderDecoratedSector { |
| RenderSolidColor(Color backgroundColor, { |
| this.desiredDeltaRadius: double.INFINITY, |
| this.desiredDeltaTheta: kTwoPi |
| }) : this.backgroundColor = backgroundColor, |
| super(new BoxDecoration(backgroundColor: backgroundColor)); |
| |
| double desiredDeltaRadius; |
| double desiredDeltaTheta; |
| final Color backgroundColor; |
| |
| SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { |
| return new SectorDimensions.withConstraints(constraints, deltaTheta: desiredDeltaTheta); |
| } |
| |
| void performLayout() { |
| deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius); |
| deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta); |
| } |
| |
| void handleEvent(PointerEvent event, HitTestEntry entry) { |
| if (event is PointerDownEvent) { |
| decoration = new BoxDecoration(backgroundColor: const Color(0xFFFF0000)); |
| } else if (event is PointerUpEvent) { |
| decoration = new BoxDecoration(backgroundColor: backgroundColor); |
| } |
| } |
| } |