| // Copyright 2017 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/widgets.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'button.dart'; |
| import 'colors.dart'; |
| import 'localizations.dart'; |
| |
| // Padding around the line at the edge of the text selection that has 0 width and |
| // the height of the text font. |
| const double _kHandlesPadding = 18.0; |
| // Minimal padding from all edges of the selection toolbar to all edges of the |
| // viewport. |
| const double _kToolbarScreenPadding = 8.0; |
| const double _kToolbarHeight = 36.0; |
| |
| const Color _kToolbarBackgroundColor = Color(0xFF2E2E2E); |
| const Color _kToolbarDividerColor = Color(0xFFB9B9B9); |
| // Read off from the output on iOS 12. This color does not vary with the |
| // application's theme color. |
| const Color _kHandlesColor = Color(0xFF136FE0); |
| |
| // This offset is used to determine the center of the selection during a drag. |
| // It's slightly below the center of the text so the finger isn't entirely |
| // covering the text being selected. |
| const Size _kSelectionOffset = Size(20.0, 30.0); |
| const Size _kToolbarTriangleSize = Size(18.0, 9.0); |
| const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0); |
| const BorderRadius _kToolbarBorderRadius = BorderRadius.all(Radius.circular(7.5)); |
| |
| const TextStyle _kToolbarButtonFontStyle = TextStyle( |
| inherit: false, |
| fontSize: 14.0, |
| letterSpacing: -0.11, |
| fontWeight: FontWeight.w300, |
| color: CupertinoColors.white, |
| ); |
| |
| /// Paints a triangle below the toolbar. |
| class _TextSelectionToolbarNotchPainter extends CustomPainter { |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = Paint() |
| ..color = _kToolbarBackgroundColor |
| ..style = PaintingStyle.fill; |
| final Path triangle = Path() |
| ..lineTo(_kToolbarTriangleSize.width / 2, 0.0) |
| ..lineTo(0.0, _kToolbarTriangleSize.height) |
| ..lineTo(-(_kToolbarTriangleSize.width / 2), 0.0) |
| ..close(); |
| canvas.drawPath(triangle, paint); |
| } |
| |
| @override |
| bool shouldRepaint(_TextSelectionToolbarNotchPainter oldPainter) => false; |
| } |
| |
| /// Manages a copy/paste text selection toolbar. |
| class _TextSelectionToolbar extends StatelessWidget { |
| const _TextSelectionToolbar({ |
| Key key, |
| this.handleCut, |
| this.handleCopy, |
| this.handlePaste, |
| this.handleSelectAll, |
| }) : super(key: key); |
| |
| final VoidCallback handleCut; |
| final VoidCallback handleCopy; |
| final VoidCallback handlePaste; |
| final VoidCallback handleSelectAll; |
| |
| @override |
| Widget build(BuildContext context) { |
| final List<Widget> items = <Widget>[]; |
| final Widget onePhysicalPixelVerticalDivider = |
| SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio); |
| final CupertinoLocalizations localizations = CupertinoLocalizations.of(context); |
| |
| if (handleCut != null) |
| items.add(_buildToolbarButton(localizations.cutButtonLabel, handleCut)); |
| |
| if (handleCopy != null) { |
| if (items.isNotEmpty) |
| items.add(onePhysicalPixelVerticalDivider); |
| items.add(_buildToolbarButton(localizations.copyButtonLabel, handleCopy)); |
| } |
| |
| if (handlePaste != null) { |
| if (items.isNotEmpty) |
| items.add(onePhysicalPixelVerticalDivider); |
| items.add(_buildToolbarButton(localizations.pasteButtonLabel, handlePaste)); |
| } |
| |
| if (handleSelectAll != null) { |
| if (items.isNotEmpty) |
| items.add(onePhysicalPixelVerticalDivider); |
| items.add(_buildToolbarButton(localizations.selectAllButtonLabel, handleSelectAll)); |
| } |
| |
| final Widget triangle = SizedBox.fromSize( |
| size: _kToolbarTriangleSize, |
| child: CustomPaint( |
| painter: _TextSelectionToolbarNotchPainter(), |
| ), |
| ); |
| |
| return Column( |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| ClipRRect( |
| borderRadius: _kToolbarBorderRadius, |
| child: DecoratedBox( |
| decoration: BoxDecoration( |
| color: _kToolbarDividerColor, |
| borderRadius: _kToolbarBorderRadius, |
| // Add a hairline border with the button color to avoid |
| // antialiasing artifacts. |
| border: Border.all(color: _kToolbarBackgroundColor, width: 0), |
| ), |
| child: Row(mainAxisSize: MainAxisSize.min, children: items), |
| ), |
| ), |
| // TODO(xster): Position the triangle based on the layout delegate, and |
| // avoid letting the triangle line up with any dividers. |
| // https://github.com/flutter/flutter/issues/11274 |
| triangle, |
| const Padding(padding: EdgeInsets.only(bottom: 10.0)), |
| ], |
| ); |
| } |
| |
| /// Builds a themed [CupertinoButton] for the toolbar. |
| CupertinoButton _buildToolbarButton(String text, VoidCallback onPressed) { |
| return CupertinoButton( |
| child: Text(text, style: _kToolbarButtonFontStyle), |
| color: _kToolbarBackgroundColor, |
| minSize: _kToolbarHeight, |
| padding: _kToolbarButtonPadding, |
| borderRadius: null, |
| pressedOpacity: 0.7, |
| onPressed: onPressed, |
| ); |
| } |
| } |
| |
| /// Centers the toolbar around the given position, ensuring that it remains on |
| /// screen. |
| class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate { |
| _TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position); |
| |
| /// The size of the screen at the time that the toolbar was last laid out. |
| final Size screenSize; |
| |
| /// Size and position of the editing region at the time the toolbar was last |
| /// laid out, in global coordinates. |
| final Rect globalEditableRegion; |
| |
| /// Anchor position of the toolbar, relative to the top left of the |
| /// [globalEditableRegion]. |
| final Offset position; |
| |
| @override |
| BoxConstraints getConstraintsForChild(BoxConstraints constraints) { |
| return constraints.loosen(); |
| } |
| |
| @override |
| Offset getPositionForChild(Size size, Size childSize) { |
| final Offset globalPosition = globalEditableRegion.topLeft + position; |
| |
| double x = globalPosition.dx - childSize.width / 2.0; |
| double y = globalPosition.dy - childSize.height; |
| |
| if (x < _kToolbarScreenPadding) |
| x = _kToolbarScreenPadding; |
| else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding) |
| x = screenSize.width - childSize.width - _kToolbarScreenPadding; |
| |
| if (y < _kToolbarScreenPadding) |
| y = _kToolbarScreenPadding; |
| else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding) |
| y = screenSize.height - childSize.height - _kToolbarScreenPadding; |
| |
| return Offset(x, y); |
| } |
| |
| @override |
| bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) { |
| return screenSize != oldDelegate.screenSize |
| || globalEditableRegion != oldDelegate.globalEditableRegion |
| || position != oldDelegate.position; |
| } |
| } |
| |
| /// Draws a single text selection handle with a bar and a ball. |
| /// |
| /// Draws from a point of origin somewhere inside the size of the painter |
| /// such that the ball is below the point of origin and the bar is above the |
| /// point of origin. |
| class _TextSelectionHandlePainter extends CustomPainter { |
| _TextSelectionHandlePainter({this.origin}); |
| |
| final Offset origin; |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = Paint() |
| ..color = _kHandlesColor |
| ..strokeWidth = 2.0; |
| // Draw circle below the origin that slightly overlaps the bar. |
| canvas.drawCircle(origin.translate(0.0, 4.0), 5.5, paint); |
| // Draw up from origin leaving 10 pixels of margin on top. |
| canvas.drawLine( |
| origin, |
| origin.translate( |
| 0.0, |
| -(size.height - 2.0 * _kHandlesPadding), |
| ), |
| paint, |
| ); |
| } |
| |
| @override |
| bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => origin != oldPainter.origin; |
| } |
| |
| class _CupertinoTextSelectionControls extends TextSelectionControls { |
| @override |
| Size handleSize = _kSelectionOffset; // Used for drag selection offset. |
| |
| /// Builder for iOS-style copy/paste text selection toolbar. |
| @override |
| Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) { |
| assert(debugCheckHasMediaQuery(context)); |
| return ConstrainedBox( |
| constraints: BoxConstraints.tight(globalEditableRegion.size), |
| child: CustomSingleChildLayout( |
| delegate: _TextSelectionToolbarLayout( |
| MediaQuery.of(context).size, |
| globalEditableRegion, |
| position, |
| ), |
| 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, |
| ), |
| ), |
| ); |
| } |
| |
| /// Builder for iOS text selection edges. |
| @override |
| Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { |
| // We want a size that's a vertical line the height of the text plus a 18.0 |
| // padding in every direction that will constitute the selection drag area. |
| final Size desiredSize = Size( |
| 2.0 * _kHandlesPadding, |
| textLineHeight + 2.0 * _kHandlesPadding, |
| ); |
| |
| final Widget handle = SizedBox.fromSize( |
| size: desiredSize, |
| child: CustomPaint( |
| painter: _TextSelectionHandlePainter( |
| // We give the painter a point of origin that's at the bottom baseline |
| // of the selection cursor position. |
| // |
| // We give it in the form of an offset from the top left of the |
| // SizedBox. |
| origin: Offset(_kHandlesPadding, textLineHeight + _kHandlesPadding), |
| ), |
| ), |
| ); |
| |
| // [buildHandle]'s widget is positioned at the selection cursor's bottom |
| // baseline. We transform the handle such that the SizedBox is superimposed |
| // on top of the text selection endpoints. |
| switch (type) { |
| case TextSelectionHandleType.left: // The left handle is upside down on iOS. |
| return Transform( |
| transform: Matrix4.rotationZ(math.pi) |
| ..translate(-_kHandlesPadding, -_kHandlesPadding), |
| child: handle, |
| ); |
| case TextSelectionHandleType.right: |
| return Transform( |
| transform: Matrix4.translationValues( |
| -_kHandlesPadding, |
| -(textLineHeight + _kHandlesPadding), |
| 0.0, |
| ), |
| child: handle, |
| ); |
| case TextSelectionHandleType.collapsed: // iOS doesn't draw anything for collapsed selections. |
| return Container(); |
| } |
| assert(type != null); |
| return null; |
| } |
| } |
| |
| /// Text selection controls that follows iOS design conventions. |
| final TextSelectionControls cupertinoTextSelectionControls = _CupertinoTextSelectionControls(); |