| // Copyright 2014 The Flutter 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/scheduler.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'debug.dart'; |
| import 'flat_button.dart'; |
| import 'icon_button.dart'; |
| import 'icons.dart'; |
| import 'material.dart'; |
| import 'material_localizations.dart'; |
| import 'theme.dart'; |
| |
| const double _kHandleSize = 22.0; |
| |
| // Minimal padding from all edges of the selection toolbar to all edges of the |
| // viewport. |
| const double _kToolbarScreenPadding = 8.0; |
| const double _kToolbarHeight = 44.0; |
| // Padding when positioning toolbar below selection. |
| const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0; |
| const double _kToolbarContentDistance = 8.0; |
| |
| /// Manages a copy/paste text selection toolbar. |
| class _TextSelectionToolbar extends StatefulWidget { |
| const _TextSelectionToolbar({ |
| Key key, |
| this.handleCut, |
| this.handleCopy, |
| this.handlePaste, |
| this.handleSelectAll, |
| this.isAbove, |
| }) : super(key: key); |
| |
| final VoidCallback handleCut; |
| final VoidCallback handleCopy; |
| final VoidCallback handlePaste; |
| final VoidCallback handleSelectAll; |
| |
| // When true, the toolbar fits above its anchor and will be positioned there. |
| final bool isAbove; |
| |
| @override |
| _TextSelectionToolbarState createState() => _TextSelectionToolbarState(); |
| } |
| |
| class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with TickerProviderStateMixin { |
| // Whether or not the overflow menu is open. When it is closed, the menu |
| // items that don't overflow are shown. When it is open, only the overflowing |
| // menu items are shown. |
| bool _overflowOpen = false; |
| |
| // The key for _TextSelectionToolbarContainer. |
| UniqueKey _containerKey = UniqueKey(); |
| |
| FlatButton _getItem(VoidCallback onPressed, String label) { |
| assert(onPressed != null); |
| return FlatButton( |
| child: Text(label), |
| onPressed: onPressed, |
| ); |
| } |
| |
| @override |
| void didUpdateWidget(_TextSelectionToolbar oldWidget) { |
| if (((widget.handleCut == null) != (oldWidget.handleCut == null)) |
| || ((widget.handleCopy == null) != (oldWidget.handleCopy == null)) |
| || ((widget.handlePaste == null) != (oldWidget.handlePaste == null)) |
| || ((widget.handleSelectAll == null) != (oldWidget.handleSelectAll == null))) { |
| // Change _TextSelectionToolbarContainer's key when the menu changes in |
| // order to cause it to rebuild. This lets it recalculate its |
| // saved width for the new set of children, and it prevents AnimatedSize |
| // from animating the size change. |
| _containerKey = UniqueKey(); |
| // If the menu items change, make sure the overflow menu is closed. This |
| // prevents an empty overflow menu. |
| _overflowOpen = false; |
| } |
| super.didUpdateWidget(oldWidget); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context); |
| final List<Widget> items = <Widget>[ |
| if (widget.handleCut != null) |
| _getItem(widget.handleCut, localizations.cutButtonLabel), |
| if (widget.handleCopy != null) |
| _getItem(widget.handleCopy, localizations.copyButtonLabel), |
| if (widget.handlePaste != null) |
| _getItem(widget.handlePaste, localizations.pasteButtonLabel), |
| if (widget.handleSelectAll != null) |
| _getItem(widget.handleSelectAll, localizations.selectAllButtonLabel), |
| ]; |
| |
| // If there is no option available, build an empty widget. |
| if (items.isEmpty) { |
| return Container(width: 0.0, height: 0.0); |
| } |
| |
| |
| return _TextSelectionToolbarContainer( |
| key: _containerKey, |
| overflowOpen: _overflowOpen, |
| child: AnimatedSize( |
| vsync: this, |
| // This duration was eyeballed on a Pixel 2 emulator running Android |
| // API 28. |
| duration: const Duration(milliseconds: 140), |
| child: Material( |
| elevation: 1.0, |
| child: _TextSelectionToolbarItems( |
| isAbove: widget.isAbove, |
| overflowOpen: _overflowOpen, |
| children: <Widget>[ |
| // The navButton that shows and hides the overflow menu is the |
| // first child. |
| Material( |
| child: IconButton( |
| // TODO(justinmc): This should be an AnimatedIcon, but |
| // AnimatedIcons doesn't yet support arrow_back to more_vert. |
| // https://github.com/flutter/flutter/issues/51209 |
| icon: Icon(_overflowOpen ? Icons.arrow_back : Icons.more_vert), |
| onPressed: () { |
| setState(() { |
| _overflowOpen = !_overflowOpen; |
| }); |
| }, |
| tooltip: _overflowOpen |
| ? localizations.backButtonTooltip |
| : localizations.moreButtonTooltip, |
| ), |
| ), |
| ...items, |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| // When the overflow menu is open, it tries to align its right edge to the right |
| // edge of the closed menu. This widget handles this effect by measuring and |
| // maintaining the width of the closed menu and aligning the child to the right. |
| class _TextSelectionToolbarContainer extends SingleChildRenderObjectWidget { |
| const _TextSelectionToolbarContainer({ |
| Key key, |
| @required Widget child, |
| @required this.overflowOpen, |
| }) : assert(child != null), |
| assert(overflowOpen != null), |
| super(key: key, child: child); |
| |
| final bool overflowOpen; |
| |
| @override |
| _TextSelectionToolbarContainerRenderBox createRenderObject(BuildContext context) { |
| return _TextSelectionToolbarContainerRenderBox(overflowOpen: overflowOpen); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _TextSelectionToolbarContainerRenderBox renderObject) { |
| renderObject.overflowOpen = overflowOpen; |
| } |
| } |
| |
| class _TextSelectionToolbarContainerRenderBox extends RenderProxyBox { |
| _TextSelectionToolbarContainerRenderBox({ |
| @required bool overflowOpen, |
| }) : assert(overflowOpen != null), |
| _overflowOpen = overflowOpen, |
| super(); |
| |
| // The width of the menu when it was closed. This is used to achieve the |
| // behavior where the open menu aligns its right edge to the closed menu's |
| // right edge. |
| double _closedWidth; |
| |
| bool _overflowOpen; |
| bool get overflowOpen => _overflowOpen; |
| set overflowOpen(bool value) { |
| if (value == overflowOpen) { |
| return; |
| } |
| _overflowOpen = value; |
| markNeedsLayout(); |
| } |
| |
| @override |
| void performLayout() { |
| child.layout(constraints.loosen(), parentUsesSize: true); |
| |
| // Save the width when the menu is closed. If the menu changes, this width |
| // is invalid, so it's important that this RenderBox be recreated in that |
| // case. Currently, this is achieved by providing a new key to |
| // _TextSelectionToolbarContainer. |
| if (!overflowOpen && _closedWidth == null) { |
| _closedWidth = child.size.width; |
| } |
| |
| size = constraints.constrain(Size( |
| // If the open menu is wider than the closed menu, just use its own width |
| // and don't worry about aligning the right edges. |
| // _closedWidth is used even when the menu is closed to allow it to |
| // animate its size while keeping the same right alignment. |
| _closedWidth == null || child.size.width > _closedWidth ? child.size.width : _closedWidth, |
| child.size.height, |
| )); |
| |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| childParentData.offset = Offset( |
| size.width - child.size.width, |
| 0.0, |
| ); |
| } |
| |
| // Paint at the offset set in the parent data. |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| context.paintChild(child, childParentData.offset + offset); |
| } |
| |
| // Include the parent data offset in the hit test. |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { Offset position }) { |
| // The x, y parameters have the top left of the node's box as the origin. |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| return result.addWithPaintOffset( |
| offset: childParentData.offset, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset transformed) { |
| assert(transformed == position - childParentData.offset); |
| return child.hitTest(result, position: transformed); |
| }, |
| ); |
| } |
| |
| @override |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! _ToolbarParentData) { |
| child.parentData = _ToolbarParentData(); |
| } |
| } |
| |
| @override |
| void applyPaintTransform(RenderObject child, Matrix4 transform) { |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| transform.translate(childParentData.offset.dx, childParentData.offset.dy); |
| super.applyPaintTransform(child, transform); |
| } |
| } |
| |
| // Renders the menu items in the correct positions in the menu and its overflow |
| // submenu based on calculating which item would first overflow. |
| class _TextSelectionToolbarItems extends MultiChildRenderObjectWidget { |
| _TextSelectionToolbarItems({ |
| Key key, |
| @required this.isAbove, |
| @required this.overflowOpen, |
| @required List<Widget> children, |
| }) : assert(children != null), |
| assert(isAbove != null), |
| assert(overflowOpen != null), |
| super(key: key, children: children); |
| |
| final bool isAbove; |
| final bool overflowOpen; |
| |
| @override |
| _TextSelectionToolbarItemsRenderBox createRenderObject(BuildContext context) { |
| return _TextSelectionToolbarItemsRenderBox( |
| isAbove: isAbove, |
| overflowOpen: overflowOpen, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _TextSelectionToolbarItemsRenderBox renderObject) { |
| renderObject |
| ..isAbove = isAbove |
| ..overflowOpen = overflowOpen; |
| } |
| |
| @override |
| _TextSelectionToolbarItemsElement createElement() => _TextSelectionToolbarItemsElement(this); |
| } |
| |
| class _ToolbarParentData extends ContainerBoxParentData<RenderBox> { |
| /// Whether or not this child is painted. |
| /// |
| /// Children in the selection toolbar may be laid out for measurement purposes |
| /// but not painted. This allows these children to be identified. |
| bool shouldPaint; |
| |
| @override |
| String toString() => '${super.toString()}; shouldPaint=$shouldPaint'; |
| } |
| |
| class _TextSelectionToolbarItemsElement extends MultiChildRenderObjectElement { |
| _TextSelectionToolbarItemsElement( |
| MultiChildRenderObjectWidget widget, |
| ) : super(widget); |
| |
| static bool _shouldPaint(Element child) { |
| return (child.renderObject.parentData as _ToolbarParentData).shouldPaint; |
| } |
| |
| @override |
| void debugVisitOnstageChildren(ElementVisitor visitor) { |
| children.where(_shouldPaint).forEach(visitor); |
| } |
| } |
| |
| class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRenderObjectMixin<RenderBox, _ToolbarParentData> { |
| _TextSelectionToolbarItemsRenderBox({ |
| @required bool isAbove, |
| @required bool overflowOpen, |
| }) : assert(overflowOpen != null), |
| assert(isAbove != null), |
| _isAbove = isAbove, |
| _overflowOpen = overflowOpen, |
| super(); |
| |
| // The index of the last item that doesn't overflow. |
| int _lastIndexThatFits = -1; |
| |
| bool _isAbove; |
| bool get isAbove => _isAbove; |
| set isAbove(bool value) { |
| if (value == isAbove) { |
| return; |
| } |
| _isAbove = value; |
| markNeedsLayout(); |
| } |
| |
| bool _overflowOpen; |
| bool get overflowOpen => _overflowOpen; |
| set overflowOpen(bool value) { |
| if (value == overflowOpen) { |
| return; |
| } |
| _overflowOpen = value; |
| markNeedsLayout(); |
| } |
| |
| // Layout the necessary children, and figure out where the children first |
| // overflow, if at all. |
| void _layoutChildren() { |
| // When overflow is not open, the toolbar is always a specific height. |
| final BoxConstraints sizedConstraints = _overflowOpen |
| ? constraints |
| : BoxConstraints.loose(Size( |
| constraints.maxWidth, |
| _kToolbarHeight, |
| )); |
| |
| int i = -1; |
| double width = 0.0; |
| visitChildren((RenderObject renderObjectChild) { |
| i++; |
| |
| // No need to layout children inside the overflow menu when it's closed. |
| // The opposite is not true. It is necessary to layout the children that |
| // don't overflow when the overflow menu is open in order to calculate |
| // _lastIndexThatFits. |
| if (_lastIndexThatFits != -1 && !overflowOpen) { |
| return; |
| } |
| |
| final RenderBox child = renderObjectChild as RenderBox; |
| child.layout(sizedConstraints.loosen(), parentUsesSize: true); |
| width += child.size.width; |
| |
| if (width > sizedConstraints.maxWidth && _lastIndexThatFits == -1) { |
| _lastIndexThatFits = i - 1; |
| } |
| }); |
| |
| // If the last child overflows, but only because of the width of the |
| // overflow button, then just show it and hide the overflow button. |
| final RenderBox navButton = firstChild; |
| if (_lastIndexThatFits != -1 |
| && _lastIndexThatFits == childCount - 2 |
| && width - navButton.size.width <= sizedConstraints.maxWidth) { |
| _lastIndexThatFits = -1; |
| } |
| } |
| |
| // Returns true when the child should be painted, false otherwise. |
| bool _shouldPaintChild(RenderObject renderObjectChild, int index) { |
| // Paint the navButton when there is overflow. |
| if (renderObjectChild == firstChild) { |
| return _lastIndexThatFits != -1; |
| } |
| |
| // If there is no overflow, all children besides the navButton are painted. |
| if (_lastIndexThatFits == -1) { |
| return true; |
| } |
| |
| // When there is overflow, paint if the child is in the part of the menu |
| // that is currently open. Overflowing children are painted when the |
| // overflow menu is open, and the children that fit are painted when the |
| // overflow menu is closed. |
| return (index > _lastIndexThatFits) == overflowOpen; |
| } |
| |
| // Decide which children will be pained and set their shouldPaint, and set the |
| // offset that painted children will be placed at. |
| void _placeChildren() { |
| int i = -1; |
| Size nextSize = const Size(0.0, 0.0); |
| double fitWidth = 0.0; |
| final RenderBox navButton = firstChild; |
| double overflowHeight = overflowOpen && !isAbove ? navButton.size.height : 0.0; |
| visitChildren((RenderObject renderObjectChild) { |
| i++; |
| |
| final RenderBox child = renderObjectChild as RenderBox; |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| |
| // Handle placing the navigation button after iterating all children. |
| if (renderObjectChild == navButton) { |
| return; |
| } |
| |
| // There is no need to place children that won't be painted. |
| if (!_shouldPaintChild(renderObjectChild, i)) { |
| childParentData.shouldPaint = false; |
| return; |
| } |
| childParentData.shouldPaint = true; |
| |
| if (!overflowOpen) { |
| childParentData.offset = Offset(fitWidth, 0.0); |
| fitWidth += child.size.width; |
| nextSize = Size( |
| fitWidth, |
| math.max(child.size.height, nextSize.height), |
| ); |
| } else { |
| childParentData.offset = Offset(0.0, overflowHeight); |
| overflowHeight += child.size.height; |
| nextSize = Size( |
| math.max(child.size.width, nextSize.width), |
| overflowHeight, |
| ); |
| } |
| }); |
| |
| // Place the navigation button if needed. |
| final _ToolbarParentData navButtonParentData = navButton.parentData as _ToolbarParentData; |
| if (_shouldPaintChild(firstChild, 0)) { |
| navButtonParentData.shouldPaint = true; |
| if (overflowOpen) { |
| navButtonParentData.offset = isAbove |
| ? Offset(0.0, overflowHeight) |
| : Offset.zero; |
| nextSize = Size( |
| nextSize.width, |
| isAbove ? nextSize.height + navButton.size.height : nextSize.height, |
| ); |
| } else { |
| navButtonParentData.offset = Offset(fitWidth, 0.0); |
| nextSize = Size(nextSize.width + navButton.size.width, nextSize.height); |
| } |
| } else { |
| navButtonParentData.shouldPaint = false; |
| } |
| |
| size = nextSize; |
| } |
| |
| @override |
| void performLayout() { |
| _lastIndexThatFits = -1; |
| if (firstChild == null) { |
| performResize(); |
| return; |
| } |
| |
| _layoutChildren(); |
| _placeChildren(); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| visitChildren((RenderObject renderObjectChild) { |
| final RenderBox child = renderObjectChild as RenderBox; |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| if (!childParentData.shouldPaint) { |
| return; |
| } |
| |
| context.paintChild(child, childParentData.offset + offset); |
| }); |
| } |
| |
| @override |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! _ToolbarParentData) { |
| child.parentData = _ToolbarParentData(); |
| } |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { Offset position }) { |
| // The x, y parameters have the top left of the node's box as the origin. |
| RenderBox child = lastChild; |
| while (child != null) { |
| final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData; |
| |
| // Don't hit test children aren't shown. |
| if (!childParentData.shouldPaint) { |
| child = childParentData.previousSibling; |
| continue; |
| } |
| |
| final bool isHit = result.addWithPaintOffset( |
| offset: childParentData.offset, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset transformed) { |
| assert(transformed == position - childParentData.offset); |
| return child.hitTest(result, position: transformed); |
| }, |
| ); |
| if (isHit) |
| return true; |
| child = childParentData.previousSibling; |
| } |
| return false; |
| } |
| } |
| |
| /// Centers the toolbar around the given anchor, ensuring that it remains on |
| /// screen. |
| class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { |
| _TextSelectionToolbarLayout(this.anchor, this.upperBounds, this.fitsAbove); |
| |
| /// Anchor position of the toolbar in global coordinates. |
| final Offset anchor; |
| |
| /// The upper-most valid y value for the anchor. |
| final double upperBounds; |
| |
| /// Whether the closed toolbar fits above the anchor position. |
| /// |
| /// If the closed toolbar doesn't fit, then the menu is rendered below the |
| /// anchor position. It should never happen that the toolbar extends below the |
| /// padded bottom of the screen. |
| /// |
| /// If the closed toolbar does fit but it doesn't fit when the overflow menu |
| /// is open, then the toolbar is still rendered above the anchor position. It |
| /// then grows downward, overlapping the selection. |
| final bool fitsAbove; |
| |
| // Return the value that centers width as closely as possible to position |
| // while fitting inside of min and max. |
| static double _centerOn(double position, double width, double min, double max) { |
| // If it overflows on the left, put it as far left as possible. |
| if (position - width / 2.0 < min) { |
| return min; |
| } |
| |
| // If it overflows on the right, put it as far right as possible. |
| if (position + width / 2.0 > max) { |
| return max - width; |
| } |
| |
| // Otherwise it fits while perfectly centered. |
| return position - width / 2.0; |
| } |
| |
| @override |
| BoxConstraints getConstraintsForChild(BoxConstraints constraints) { |
| return constraints.loosen(); |
| } |
| |
| @override |
| Offset getPositionForChild(Size size, Size childSize) { |
| return Offset( |
| _centerOn( |
| anchor.dx, |
| childSize.width, |
| _kToolbarScreenPadding, |
| size.width - _kToolbarScreenPadding, |
| ), |
| fitsAbove |
| ? math.max(upperBounds, anchor.dy - childSize.height) |
| : anchor.dy, |
| ); |
| } |
| |
| @override |
| bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) { |
| return anchor != oldDelegate.anchor; |
| } |
| } |
| |
| /// Draws a single text selection handle which points up and to the left. |
| class _TextSelectionHandlePainter extends CustomPainter { |
| _TextSelectionHandlePainter({ this.color }); |
| |
| final Color color; |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = Paint()..color = color; |
| final double radius = size.width/2.0; |
| final Rect circle = Rect.fromCircle(center: Offset(radius, radius), radius: radius); |
| final Rect point = Rect.fromLTWH(0.0, 0.0, radius, radius); |
| final Path path = Path()..addOval(circle)..addRect(point); |
| canvas.drawPath(path, paint); |
| } |
| |
| @override |
| bool shouldRepaint(_TextSelectionHandlePainter oldPainter) { |
| return color != oldPainter.color; |
| } |
| } |
| |
| class _MaterialTextSelectionControls extends TextSelectionControls { |
| /// Returns the size of the Material handle. |
| @override |
| Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize); |
| |
| /// Builder for material-style copy/paste text selection toolbar. |
| @override |
| Widget buildToolbar( |
| BuildContext context, |
| Rect globalEditableRegion, |
| double textLineHeight, |
| Offset selectionMidpoint, |
| List<TextSelectionPoint> endpoints, |
| TextSelectionDelegate delegate, |
| ) { |
| assert(debugCheckHasMediaQuery(context)); |
| assert(debugCheckHasMaterialLocalizations(context)); |
| |
| // The toolbar should appear below the TextField when there is not enough |
| // space above the TextField to show it. |
| final TextSelectionPoint startTextSelectionPoint = endpoints[0]; |
| final TextSelectionPoint endTextSelectionPoint = endpoints.length > 1 |
| ? endpoints[1] |
| : endpoints[0]; |
| const double closedToolbarHeightNeeded = _kToolbarScreenPadding |
| + _kToolbarHeight |
| + _kToolbarContentDistance; |
| final double paddingTop = MediaQuery.of(context).padding.top; |
| final double availableHeight = globalEditableRegion.top |
| + startTextSelectionPoint.point.dy |
| - textLineHeight |
| - paddingTop; |
| final bool fitsAbove = closedToolbarHeightNeeded <= availableHeight; |
| final Offset anchor = Offset( |
| globalEditableRegion.left + selectionMidpoint.dx, |
| fitsAbove |
| ? globalEditableRegion.top + startTextSelectionPoint.point.dy - textLineHeight - _kToolbarContentDistance |
| : globalEditableRegion.top + endTextSelectionPoint.point.dy + _kToolbarContentDistanceBelow, |
| ); |
| |
| return Stack( |
| children: <Widget>[ |
| CustomSingleChildLayout( |
| delegate: _TextSelectionToolbarLayout( |
| anchor, |
| _kToolbarScreenPadding + paddingTop, |
| fitsAbove, |
| ), |
| child: _TextSelectionToolbar( |
| handleCut: canCut(delegate) ? () => handleCut(delegate) : null, |
| handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, |
| handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, |
| handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, |
| isAbove: fitsAbove, |
| ), |
| ), |
| ], |
| ); |
| } |
| |
| /// Builder for material-style text selection handles. |
| @override |
| Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) { |
| final Widget handle = SizedBox( |
| width: _kHandleSize, |
| height: _kHandleSize, |
| child: CustomPaint( |
| painter: _TextSelectionHandlePainter( |
| color: Theme.of(context).textSelectionHandleColor, |
| ), |
| ), |
| ); |
| |
| // [handle] is a circle, with a rectangle in the top left quadrant of that |
| // circle (an onion pointing to 10:30). We rotate [handle] to point |
| // straight up or up-right depending on the handle type. |
| switch (type) { |
| case TextSelectionHandleType.left: // points up-right |
| return Transform.rotate( |
| angle: math.pi / 2.0, |
| child: handle, |
| ); |
| case TextSelectionHandleType.right: // points up-left |
| return handle; |
| case TextSelectionHandleType.collapsed: // points up |
| return Transform.rotate( |
| angle: math.pi / 4.0, |
| child: handle, |
| ); |
| } |
| assert(type != null); |
| return null; |
| } |
| |
| /// Gets anchor for material-style text selection handles. |
| /// |
| /// See [TextSelectionControls.getHandleAnchor]. |
| @override |
| Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) { |
| switch (type) { |
| case TextSelectionHandleType.left: |
| return const Offset(_kHandleSize, 0); |
| case TextSelectionHandleType.right: |
| return Offset.zero; |
| default: |
| return const Offset(_kHandleSize / 2, -4); |
| } |
| } |
| |
| @override |
| bool canSelectAll(TextSelectionDelegate delegate) { |
| // Android allows SelectAll when selection is not collapsed, unless |
| // everything has already been selected. |
| final TextEditingValue value = delegate.textEditingValue; |
| return delegate.selectAllEnabled && |
| value.text.isNotEmpty && |
| !(value.selection.start == 0 && value.selection.end == value.text.length); |
| } |
| } |
| |
| /// Text selection controls that follow the Material Design specification. |
| final TextSelectionControls materialTextSelectionControls = _MaterialTextSelectionControls(); |