| 1 | // Copyright 2014 The Flutter 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 | |
| 5 | /// @docImport 'package:flutter/cupertino.dart'; |
| 6 | /// @docImport 'package:flutter/material.dart'; |
| 7 | /// |
| 8 | /// @docImport 'basic.dart'; |
| 9 | library; |
| 10 | |
| 11 | import 'dart:math' as math; |
| 12 | |
| 13 | import 'package:flutter/rendering.dart'; |
| 14 | |
| 15 | /// A [SingleChildLayoutDelegate] for use with [CustomSingleChildLayout] that |
| 16 | /// positions its child above [anchorAbove] if it fits, or otherwise below |
| 17 | /// [anchorBelow]. |
| 18 | /// |
| 19 | /// Primarily intended for use with toolbars or context menus. |
| 20 | /// |
| 21 | /// See also: |
| 22 | /// |
| 23 | /// * [TextSelectionToolbar], which uses this to position itself. |
| 24 | /// * [CupertinoTextSelectionToolbar], which also uses this to position |
| 25 | /// itself. |
| 26 | class TextSelectionToolbarLayoutDelegate extends SingleChildLayoutDelegate { |
| 27 | /// Creates an instance of TextSelectionToolbarLayoutDelegate. |
| 28 | /// |
| 29 | /// The [fitsAbove] parameter is optional; if omitted, it will be calculated. |
| 30 | TextSelectionToolbarLayoutDelegate({ |
| 31 | required this.anchorAbove, |
| 32 | required this.anchorBelow, |
| 33 | this.fitsAbove, |
| 34 | }); |
| 35 | |
| 36 | /// {@macro flutter.material.TextSelectionToolbar.anchorAbove} |
| 37 | /// |
| 38 | /// Should be provided in local coordinates. |
| 39 | final Offset anchorAbove; |
| 40 | |
| 41 | /// {@macro flutter.material.TextSelectionToolbar.anchorAbove} |
| 42 | /// |
| 43 | /// Should be provided in local coordinates. |
| 44 | final Offset anchorBelow; |
| 45 | |
| 46 | /// Whether or not the child should be considered to fit above anchorAbove. |
| 47 | /// |
| 48 | /// Typically used to force the child to be drawn at anchorAbove even when it |
| 49 | /// doesn't fit, such as when the Material [TextSelectionToolbar] draws an |
| 50 | /// open overflow menu. |
| 51 | /// |
| 52 | /// If not provided, it will be calculated. |
| 53 | final bool? fitsAbove; |
| 54 | |
| 55 | /// Return the distance from zero that centers `width` as closely as possible |
| 56 | /// to `position` from zero while fitting between zero and `max`. |
| 57 | static double centerOn(double position, double width, double max) { |
| 58 | // If it overflows on the left, put it as far left as possible. |
| 59 | if (position - width / 2.0 < 0.0) { |
| 60 | return 0.0; |
| 61 | } |
| 62 | |
| 63 | // If it overflows on the right, put it as far right as possible. |
| 64 | if (position + width / 2.0 > max) { |
| 65 | return max - width; |
| 66 | } |
| 67 | |
| 68 | // Otherwise it fits while perfectly centered. |
| 69 | return position - width / 2.0; |
| 70 | } |
| 71 | |
| 72 | @override |
| 73 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { |
| 74 | return constraints.loosen(); |
| 75 | } |
| 76 | |
| 77 | @override |
| 78 | Offset getPositionForChild(Size size, Size childSize) { |
| 79 | final bool fitsAbove = this.fitsAbove ?? anchorAbove.dy >= childSize.height; |
| 80 | final Offset anchor = fitsAbove ? anchorAbove : anchorBelow; |
| 81 | |
| 82 | return Offset( |
| 83 | centerOn(anchor.dx, childSize.width, size.width), |
| 84 | fitsAbove ? math.max(0.0, anchor.dy - childSize.height) : anchor.dy, |
| 85 | ); |
| 86 | } |
| 87 | |
| 88 | @override |
| 89 | bool shouldRelayout(TextSelectionToolbarLayoutDelegate oldDelegate) { |
| 90 | return anchorAbove != oldDelegate.anchorAbove || |
| 91 | anchorBelow != oldDelegate.anchorBelow || |
| 92 | fitsAbove != oldDelegate.fitsAbove; |
| 93 | } |
| 94 | } |
| 95 | |