• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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