1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:math' as math; 6 7import 'package:flutter/rendering.dart'; 8import 'package:flutter/gestures.dart'; 9import 'package:meta/meta.dart'; 10 11const double kTwoPi = 2 * math.pi; 12 13class SectorConstraints extends Constraints { 14 const SectorConstraints({ 15 this.minDeltaRadius = 0.0, 16 this.maxDeltaRadius = double.infinity, 17 this.minDeltaTheta = 0.0, 18 this.maxDeltaTheta = kTwoPi, 19 }) : assert(maxDeltaRadius >= minDeltaRadius), 20 assert(maxDeltaTheta >= minDeltaTheta); 21 22 const SectorConstraints.tight({ double deltaRadius = 0.0, double deltaTheta = 0.0 }) 23 : minDeltaRadius = deltaRadius, 24 maxDeltaRadius = deltaRadius, 25 minDeltaTheta = deltaTheta, 26 maxDeltaTheta = deltaTheta; 27 28 final double minDeltaRadius; 29 final double maxDeltaRadius; 30 final double minDeltaTheta; 31 final double maxDeltaTheta; 32 33 double constrainDeltaRadius(double deltaRadius) { 34 return deltaRadius.clamp(minDeltaRadius, maxDeltaRadius); 35 } 36 37 double constrainDeltaTheta(double deltaTheta) { 38 return deltaTheta.clamp(minDeltaTheta, maxDeltaTheta); 39 } 40 41 @override 42 bool get isTight => minDeltaTheta >= maxDeltaTheta && minDeltaTheta >= maxDeltaTheta; 43 44 @override 45 bool get isNormalized => minDeltaRadius <= maxDeltaRadius && minDeltaTheta <= maxDeltaTheta; 46 47 @override 48 bool debugAssertIsValid({ 49 bool isAppliedConstraint = false, 50 InformationCollector informationCollector, 51 }) { 52 assert(isNormalized); 53 return isNormalized; 54 } 55} 56 57class SectorDimensions { 58 const SectorDimensions({ this.deltaRadius = 0.0, this.deltaTheta = 0.0 }); 59 60 factory SectorDimensions.withConstraints( 61 SectorConstraints constraints, { 62 double deltaRadius = 0.0, 63 double deltaTheta = 0.0, 64 }) { 65 return SectorDimensions( 66 deltaRadius: constraints.constrainDeltaRadius(deltaRadius), 67 deltaTheta: constraints.constrainDeltaTheta(deltaTheta), 68 ); 69 } 70 71 final double deltaRadius; 72 final double deltaTheta; 73} 74 75class SectorParentData extends ParentData { 76 double radius = 0.0; 77 double theta = 0.0; 78} 79 80/// Base class for [RenderObject]s that live in a polar coordinate space. 81/// 82/// In a polar coordinate system each point on a plane is determined by a 83/// distance from a reference point ("radius") and an angle from a reference 84/// direction ("theta"). 85/// 86/// See also: 87/// 88/// * <https://en.wikipedia.org/wiki/Polar_coordinate_system>, which defines 89/// the polar coordinate space. 90/// * [RenderBox], which is the base class for [RenderObject]s that live in a 91/// cartesian coordinate space. 92abstract class RenderSector extends RenderObject { 93 94 @override 95 void setupParentData(RenderObject child) { 96 if (child.parentData is! SectorParentData) 97 child.parentData = SectorParentData(); 98 } 99 100 // RenderSectors always use SectorParentData subclasses, as they need to be 101 // able to read their position information for painting and hit testing. 102 @override 103 SectorParentData get parentData => super.parentData; 104 105 SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { 106 return SectorDimensions.withConstraints(constraints); 107 } 108 109 @override 110 SectorConstraints get constraints => super.constraints; 111 112 @override 113 void debugAssertDoesMeetConstraints() { 114 assert(constraints != null); 115 assert(deltaRadius != null); 116 assert(deltaRadius < double.infinity); 117 assert(deltaTheta != null); 118 assert(deltaTheta < double.infinity); 119 assert(constraints.minDeltaRadius <= deltaRadius); 120 assert(deltaRadius <= math.max(constraints.minDeltaRadius, constraints.maxDeltaRadius)); 121 assert(constraints.minDeltaTheta <= deltaTheta); 122 assert(deltaTheta <= math.max(constraints.minDeltaTheta, constraints.maxDeltaTheta)); 123 } 124 125 @override 126 void performResize() { 127 // default behavior for subclasses that have sizedByParent = true 128 deltaRadius = constraints.constrainDeltaRadius(0.0); 129 deltaTheta = constraints.constrainDeltaTheta(0.0); 130 } 131 132 @override 133 void performLayout() { 134 // descendants have to either override performLayout() to set both 135 // the dimensions and lay out children, or, set sizedByParent to 136 // true so that performResize()'s logic above does its thing. 137 assert(sizedByParent); 138 } 139 140 @override 141 Rect get paintBounds => Rect.fromLTWH(0.0, 0.0, 2.0 * deltaRadius, 2.0 * deltaRadius); 142 143 @override 144 Rect get semanticBounds => Rect.fromLTWH(-deltaRadius, -deltaRadius, 2.0 * deltaRadius, 2.0 * deltaRadius); 145 146 bool hitTest(SectorHitTestResult result, { double radius, double theta }) { 147 if (radius < parentData.radius || radius >= parentData.radius + deltaRadius || 148 theta < parentData.theta || theta >= parentData.theta + deltaTheta) 149 return false; 150 hitTestChildren(result, radius: radius, theta: theta); 151 result.add(SectorHitTestEntry(this, radius: radius, theta: theta)); 152 return true; 153 } 154 void hitTestChildren(SectorHitTestResult result, { double radius, double theta }) { } 155 156 double deltaRadius; 157 double deltaTheta; 158} 159 160abstract class RenderDecoratedSector extends RenderSector { 161 162 RenderDecoratedSector(BoxDecoration decoration) : _decoration = decoration; 163 164 BoxDecoration _decoration; 165 BoxDecoration get decoration => _decoration; 166 set decoration(BoxDecoration value) { 167 if (value == _decoration) 168 return; 169 _decoration = value; 170 markNeedsPaint(); 171 } 172 173 // offset must point to the center of the circle 174 @override 175 void paint(PaintingContext context, Offset offset) { 176 assert(deltaRadius != null); 177 assert(deltaTheta != null); 178 assert(parentData is SectorParentData); 179 180 if (_decoration == null) 181 return; 182 183 if (_decoration.color != null) { 184 final Canvas canvas = context.canvas; 185 final Paint paint = Paint()..color = _decoration.color; 186 final Path path = Path(); 187 final double outerRadius = parentData.radius + deltaRadius; 188 final Rect outerBounds = Rect.fromLTRB(offset.dx-outerRadius, offset.dy-outerRadius, offset.dx+outerRadius, offset.dy+outerRadius); 189 path.arcTo(outerBounds, parentData.theta, deltaTheta, true); 190 final double innerRadius = parentData.radius; 191 final Rect innerBounds = Rect.fromLTRB(offset.dx-innerRadius, offset.dy-innerRadius, offset.dx+innerRadius, offset.dy+innerRadius); 192 path.arcTo(innerBounds, parentData.theta + deltaTheta, -deltaTheta, false); 193 path.close(); 194 canvas.drawPath(path, paint); 195 } 196 } 197 198} 199 200class SectorChildListParentData extends SectorParentData with ContainerParentDataMixin<RenderSector> { } 201 202class RenderSectorWithChildren extends RenderDecoratedSector with ContainerRenderObjectMixin<RenderSector, SectorChildListParentData> { 203 RenderSectorWithChildren(BoxDecoration decoration) : super(decoration); 204 205 @override 206 void hitTestChildren(SectorHitTestResult result, { double radius, double theta }) { 207 RenderSector child = lastChild; 208 while (child != null) { 209 if (child.hitTest(result, radius: radius, theta: theta)) 210 return; 211 final SectorChildListParentData childParentData = child.parentData; 212 child = childParentData.previousSibling; 213 } 214 } 215 216 @override 217 void visitChildren(RenderObjectVisitor visitor) { 218 RenderSector child = lastChild; 219 while (child != null) { 220 visitor(child); 221 final SectorChildListParentData childParentData = child.parentData; 222 child = childParentData.previousSibling; 223 } 224 } 225} 226 227class RenderSectorRing extends RenderSectorWithChildren { 228 // lays out RenderSector children in a ring 229 230 RenderSectorRing({ 231 BoxDecoration decoration, 232 double deltaRadius = double.infinity, 233 double padding = 0.0, 234 }) : _padding = padding, 235 assert(deltaRadius >= 0.0), 236 _desiredDeltaRadius = deltaRadius, 237 super(decoration); 238 239 double _desiredDeltaRadius; 240 double get desiredDeltaRadius => _desiredDeltaRadius; 241 set desiredDeltaRadius(double value) { 242 assert(value != null); 243 assert(value >= 0); 244 if (_desiredDeltaRadius != value) { 245 _desiredDeltaRadius = value; 246 markNeedsLayout(); 247 } 248 } 249 250 double _padding; 251 double get padding => _padding; 252 set padding(double value) { 253 // TODO(ianh): avoid code duplication 254 assert(value != null); 255 if (_padding != value) { 256 _padding = value; 257 markNeedsLayout(); 258 } 259 } 260 261 @override 262 void setupParentData(RenderObject child) { 263 // TODO(ianh): avoid code duplication 264 if (child.parentData is! SectorChildListParentData) 265 child.parentData = SectorChildListParentData(); 266 } 267 268 @override 269 SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { 270 final double outerDeltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius); 271 final double innerDeltaRadius = math.max(0.0, outerDeltaRadius - padding * 2.0); 272 final double childRadius = radius + padding; 273 final double paddingTheta = math.atan(padding / (radius + outerDeltaRadius)); 274 double innerTheta = paddingTheta; // increments with each child 275 double remainingDeltaTheta = math.max(0.0, constraints.maxDeltaTheta - (innerTheta + paddingTheta)); 276 RenderSector child = firstChild; 277 while (child != null) { 278 final SectorConstraints innerConstraints = SectorConstraints( 279 maxDeltaRadius: innerDeltaRadius, 280 maxDeltaTheta: remainingDeltaTheta, 281 ); 282 final SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius); 283 innerTheta += childDimensions.deltaTheta; 284 remainingDeltaTheta -= childDimensions.deltaTheta; 285 final SectorChildListParentData childParentData = child.parentData; 286 child = childParentData.nextSibling; 287 if (child != null) { 288 innerTheta += paddingTheta; 289 remainingDeltaTheta -= paddingTheta; 290 } 291 } 292 return SectorDimensions.withConstraints(constraints, 293 deltaRadius: outerDeltaRadius, 294 deltaTheta: innerTheta); 295 } 296 297 @override 298 void performLayout() { 299 assert(parentData is SectorParentData); 300 deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius); 301 assert(deltaRadius < double.infinity); 302 final double innerDeltaRadius = deltaRadius - padding * 2.0; 303 final double childRadius = parentData.radius + padding; 304 final double paddingTheta = math.atan(padding / (parentData.radius + deltaRadius)); 305 double innerTheta = paddingTheta; // increments with each child 306 double remainingDeltaTheta = constraints.maxDeltaTheta - (innerTheta + paddingTheta); 307 RenderSector child = firstChild; 308 while (child != null) { 309 final SectorConstraints innerConstraints = SectorConstraints( 310 maxDeltaRadius: innerDeltaRadius, 311 maxDeltaTheta: remainingDeltaTheta, 312 ); 313 assert(child.parentData is SectorParentData); 314 child.parentData.theta = innerTheta; 315 child.parentData.radius = childRadius; 316 child.layout(innerConstraints, parentUsesSize: true); 317 innerTheta += child.deltaTheta; 318 remainingDeltaTheta -= child.deltaTheta; 319 final SectorChildListParentData childParentData = child.parentData; 320 child = childParentData.nextSibling; 321 if (child != null) { 322 innerTheta += paddingTheta; 323 remainingDeltaTheta -= paddingTheta; 324 } 325 } 326 deltaTheta = innerTheta; 327 } 328 329 // offset must point to the center of our circle 330 // each sector then knows how to paint itself at its location 331 @override 332 void paint(PaintingContext context, Offset offset) { 333 // TODO(ianh): avoid code duplication 334 super.paint(context, offset); 335 RenderSector child = firstChild; 336 while (child != null) { 337 context.paintChild(child, offset); 338 final SectorChildListParentData childParentData = child.parentData; 339 child = childParentData.nextSibling; 340 } 341 } 342 343} 344 345class RenderSectorSlice extends RenderSectorWithChildren { 346 // lays out RenderSector children in a stack 347 348 RenderSectorSlice({ 349 BoxDecoration decoration, 350 double deltaTheta = kTwoPi, 351 double padding = 0.0, 352 }) : _padding = padding, _desiredDeltaTheta = deltaTheta, super(decoration); 353 354 double _desiredDeltaTheta; 355 double get desiredDeltaTheta => _desiredDeltaTheta; 356 set desiredDeltaTheta(double value) { 357 assert(value != null); 358 if (_desiredDeltaTheta != value) { 359 _desiredDeltaTheta = value; 360 markNeedsLayout(); 361 } 362 } 363 364 double _padding; 365 double get padding => _padding; 366 set padding(double value) { 367 // TODO(ianh): avoid code duplication 368 assert(value != null); 369 if (_padding != value) { 370 _padding = value; 371 markNeedsLayout(); 372 } 373 } 374 375 @override 376 void setupParentData(RenderObject child) { 377 // TODO(ianh): avoid code duplication 378 if (child.parentData is! SectorChildListParentData) 379 child.parentData = SectorChildListParentData(); 380 } 381 382 @override 383 SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { 384 assert(parentData is SectorParentData); 385 final double paddingTheta = math.atan(padding / parentData.radius); 386 final double outerDeltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta); 387 final double innerDeltaTheta = outerDeltaTheta - paddingTheta * 2.0; 388 double childRadius = parentData.radius + padding; 389 double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0); 390 RenderSector child = firstChild; 391 while (child != null) { 392 final SectorConstraints innerConstraints = SectorConstraints( 393 maxDeltaRadius: remainingDeltaRadius, 394 maxDeltaTheta: innerDeltaTheta, 395 ); 396 final SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius); 397 childRadius += childDimensions.deltaRadius; 398 remainingDeltaRadius -= childDimensions.deltaRadius; 399 final SectorChildListParentData childParentData = child.parentData; 400 child = childParentData.nextSibling; 401 childRadius += padding; 402 remainingDeltaRadius -= padding; 403 } 404 return SectorDimensions.withConstraints(constraints, 405 deltaRadius: childRadius - parentData.radius, 406 deltaTheta: outerDeltaTheta); 407 } 408 409 @override 410 void performLayout() { 411 assert(parentData is SectorParentData); 412 deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta); 413 assert(deltaTheta <= kTwoPi); 414 final double paddingTheta = math.atan(padding / parentData.radius); 415 final double innerTheta = parentData.theta + paddingTheta; 416 final double innerDeltaTheta = deltaTheta - paddingTheta * 2.0; 417 double childRadius = parentData.radius + padding; 418 double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0); 419 RenderSector child = firstChild; 420 while (child != null) { 421 final SectorConstraints innerConstraints = SectorConstraints( 422 maxDeltaRadius: remainingDeltaRadius, 423 maxDeltaTheta: innerDeltaTheta, 424 ); 425 child.parentData.theta = innerTheta; 426 child.parentData.radius = childRadius; 427 child.layout(innerConstraints, parentUsesSize: true); 428 childRadius += child.deltaRadius; 429 remainingDeltaRadius -= child.deltaRadius; 430 final SectorChildListParentData childParentData = child.parentData; 431 child = childParentData.nextSibling; 432 childRadius += padding; 433 remainingDeltaRadius -= padding; 434 } 435 deltaRadius = childRadius - parentData.radius; 436 } 437 438 // offset must point to the center of our circle 439 // each sector then knows how to paint itself at its location 440 @override 441 void paint(PaintingContext context, Offset offset) { 442 // TODO(ianh): avoid code duplication 443 super.paint(context, offset); 444 RenderSector child = firstChild; 445 while (child != null) { 446 assert(child.parentData is SectorChildListParentData); 447 context.paintChild(child, offset); 448 final SectorChildListParentData childParentData = child.parentData; 449 child = childParentData.nextSibling; 450 } 451 } 452 453} 454 455class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChildMixin<RenderSector> { 456 457 RenderBoxToRenderSectorAdapter({ double innerRadius = 0.0, RenderSector child }) 458 : _innerRadius = innerRadius { 459 this.child = child; 460 } 461 462 double _innerRadius; 463 double get innerRadius => _innerRadius; 464 set innerRadius(double value) { 465 _innerRadius = value; 466 markNeedsLayout(); 467 } 468 469 @override 470 void setupParentData(RenderObject child) { 471 if (child.parentData is! SectorParentData) 472 child.parentData = SectorParentData(); 473 } 474 475 @override 476 double computeMinIntrinsicWidth(double height) { 477 if (child == null) 478 return 0.0; 479 return getIntrinsicDimensions(height: height).width; 480 } 481 482 @override 483 double computeMaxIntrinsicWidth(double height) { 484 if (child == null) 485 return 0.0; 486 return getIntrinsicDimensions(height: height).width; 487 } 488 489 @override 490 double computeMinIntrinsicHeight(double width) { 491 if (child == null) 492 return 0.0; 493 return getIntrinsicDimensions(width: width).height; 494 } 495 496 @override 497 double computeMaxIntrinsicHeight(double width) { 498 if (child == null) 499 return 0.0; 500 return getIntrinsicDimensions(width: width).height; 501 } 502 503 Size getIntrinsicDimensions({ 504 double width = double.infinity, 505 double height = double.infinity, 506 }) { 507 assert(child is RenderSector); 508 assert(child.parentData is SectorParentData); 509 assert(width != null); 510 assert(height != null); 511 if (!width.isFinite && !height.isFinite) 512 return Size.zero; 513 final double maxChildDeltaRadius = math.max(0.0, math.min(width, height) / 2.0 - innerRadius); 514 final SectorDimensions childDimensions = child.getIntrinsicDimensions(SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), innerRadius); 515 final double dimension = (innerRadius + childDimensions.deltaRadius) * 2.0; 516 return Size.square(dimension); 517 } 518 519 @override 520 void performLayout() { 521 if (child == null || (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight)) { 522 size = constraints.constrain(Size.zero); 523 child?.layout(SectorConstraints(maxDeltaRadius: innerRadius), parentUsesSize: true); 524 return; 525 } 526 assert(child is RenderSector); 527 assert(child.parentData is SectorParentData); 528 final double maxChildDeltaRadius = math.min(constraints.maxWidth, constraints.maxHeight) / 2.0 - innerRadius; 529 child.parentData.radius = innerRadius; 530 child.parentData.theta = 0.0; 531 child.layout(SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), parentUsesSize: true); 532 final double dimension = (innerRadius + child.deltaRadius) * 2.0; 533 size = constraints.constrain(Size(dimension, dimension)); 534 } 535 536 @override 537 void paint(PaintingContext context, Offset offset) { 538 super.paint(context, offset); 539 if (child != null) { 540 final Rect bounds = offset & size; 541 // we move the offset to the center of the circle for the RenderSectors 542 context.paintChild(child, bounds.center); 543 } 544 } 545 546 @override 547 bool hitTest(BoxHitTestResult result, { Offset position }) { 548 if (child == null) 549 return false; 550 double x = position.dx; 551 double y = position.dy; 552 // translate to our origin 553 x -= size.width / 2.0; 554 y -= size.height / 2.0; 555 // convert to radius/theta 556 final double radius = math.sqrt(x * x + y * y); 557 final double theta = (math.atan2(x, -y) - math.pi / 2.0) % kTwoPi; 558 if (radius < innerRadius) 559 return false; 560 if (radius >= innerRadius + child.deltaRadius) 561 return false; 562 if (theta > child.deltaTheta) 563 return false; 564 child.hitTest(SectorHitTestResult.wrap(result), radius: radius, theta: theta); 565 result.add(BoxHitTestEntry(this, position)); 566 return true; 567 } 568 569} 570 571class RenderSolidColor extends RenderDecoratedSector { 572 RenderSolidColor( 573 this.backgroundColor, { 574 this.desiredDeltaRadius = double.infinity, 575 this.desiredDeltaTheta = kTwoPi, 576 }) : super(BoxDecoration(color: backgroundColor)); 577 578 double desiredDeltaRadius; 579 double desiredDeltaTheta; 580 final Color backgroundColor; 581 582 @override 583 SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) { 584 return SectorDimensions.withConstraints(constraints, deltaTheta: desiredDeltaTheta); 585 } 586 587 @override 588 void performLayout() { 589 deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius); 590 deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta); 591 } 592 593 @override 594 void handleEvent(PointerEvent event, HitTestEntry entry) { 595 if (event is PointerDownEvent) { 596 decoration = const BoxDecoration(color: Color(0xFFFF0000)); 597 } else if (event is PointerUpEvent) { 598 decoration = BoxDecoration(color: backgroundColor); 599 } 600 } 601} 602 603/// The result of performing a hit test on [RenderSector]s. 604class SectorHitTestResult extends HitTestResult { 605 /// Creates an empty hit test result for hit testing on [RenderSector]. 606 SectorHitTestResult() : super(); 607 608 /// Wraps `result` to create a [HitTestResult] that implements the 609 /// [SectorHitTestResult] protocol for hit testing on [RenderSector]s. 610 /// 611 /// This method is used by [RenderObject]s that adapt between the 612 /// [RenderSector]-world and the non-[RenderSector]-world to convert a (subtype of) 613 /// [HitTestResult] to a [SectorHitTestResult] for hit testing on [RenderSector]s. 614 /// 615 /// The [HitTestEntry]s added to the returned [SectorHitTestResult] are also 616 /// added to the wrapped `result` (both share the same underlying data 617 /// structure to store [HitTestEntry]s). 618 /// 619 /// See also: 620 /// 621 /// * [HitTestResult.wrap], which turns a [SectorHitTestResult] back into a 622 /// generic [HitTestResult]. 623 SectorHitTestResult.wrap(HitTestResult result) : super.wrap(result); 624 625 // TODO(goderbauer): Add convenience methods to transform hit test positions 626 // once we have RenderSector implementations that move the origin of their 627 // children (e.g. RenderSectorTransform analogs to RenderTransform). 628} 629 630/// A hit test entry used by [RenderSector]. 631class SectorHitTestEntry extends HitTestEntry { 632 /// Creates a box hit test entry. 633 /// 634 /// The [radius] and [theta] argument must not be null. 635 SectorHitTestEntry(RenderSector target, { @required this.radius, @required this.theta }) 636 : assert(radius != null), 637 assert(theta != null), 638 super(target); 639 640 @override 641 RenderSector get target => super.target; 642 643 /// The radius component of the hit test position in the local coordinates of 644 /// [target]. 645 final double radius; 646 647 /// The theta component of the hit test position in the local coordinates of 648 /// [target]. 649 final double theta; 650} 651