"Fossies" - the Fresh Open Source Software Archive

Member "flutter-1.22.4/packages/flutter/lib/src/cupertino/text_selection.dart" (13 Nov 2020, 42205 Bytes) of package /linux/misc/flutter-1.22.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    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 // @dart = 2.8
    6 
    7 import 'dart:collection';
    8 import 'dart:math' as math;
    9 import 'dart:ui' as ui;
   10 
   11 import 'package:flutter/widgets.dart';
   12 import 'package:flutter/rendering.dart';
   13 import 'package:flutter/services.dart';
   14 
   15 import 'button.dart';
   16 import 'colors.dart';
   17 import 'localizations.dart';
   18 import 'theme.dart';
   19 
   20 // Read off from the output on iOS 12. This color does not vary with the
   21 // application's theme color.
   22 const double _kSelectionHandleOverlap = 1.5;
   23 // Extracted from https://developer.apple.com/design/resources/.
   24 const double _kSelectionHandleRadius = 6;
   25 
   26 // Minimal padding from all edges of the selection toolbar to all edges of the
   27 // screen.
   28 const double _kToolbarScreenPadding = 8.0;
   29 // Minimal padding from tip of the selection toolbar arrow to horizontal edges of the
   30 // screen. Eyeballed value.
   31 const double _kArrowScreenPadding = 26.0;
   32 
   33 // Vertical distance between the tip of the arrow and the line of text the arrow
   34 // is pointing to. The value used here is eyeballed.
   35 const double _kToolbarContentDistance = 8.0;
   36 // Values derived from https://developer.apple.com/design/resources/.
   37 // 92% Opacity ~= 0xEB
   38 
   39 // Values extracted from https://developer.apple.com/design/resources/.
   40 // The height of the toolbar, including the arrow.
   41 const double _kToolbarHeight = 43.0;
   42 const Size _kToolbarArrowSize = Size(14.0, 7.0);
   43 const Radius _kToolbarBorderRadius = Radius.circular(8);
   44 // Colors extracted from https://developer.apple.com/design/resources/.
   45 // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
   46 const Color _kToolbarBackgroundColor = Color(0xEB202020);
   47 const Color _kToolbarDividerColor = Color(0xFF808080);
   48 
   49 const TextStyle _kToolbarButtonFontStyle = TextStyle(
   50   inherit: false,
   51   fontSize: 14.0,
   52   letterSpacing: -0.15,
   53   fontWeight: FontWeight.w400,
   54   color: CupertinoColors.white,
   55 );
   56 
   57 const TextStyle _kToolbarButtonDisabledFontStyle = TextStyle(
   58   inherit: false,
   59   fontSize: 14.0,
   60   letterSpacing: -0.15,
   61   fontWeight: FontWeight.w400,
   62   color: CupertinoColors.inactiveGray,
   63 );
   64 
   65 // Eyeballed value.
   66 const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0);
   67 
   68 // Generates the child that's passed into CupertinoTextSelectionToolbar.
   69 class _CupertinoTextSelectionToolbarWrapper extends StatefulWidget {
   70   const _CupertinoTextSelectionToolbarWrapper({
   71     Key key,
   72     this.arrowTipX,
   73     this.barTopY,
   74     this.clipboardStatus,
   75     this.handleCut,
   76     this.handleCopy,
   77     this.handlePaste,
   78     this.handleSelectAll,
   79     this.isArrowPointingDown,
   80   }) : super(key: key);
   81 
   82   final double arrowTipX;
   83   final double barTopY;
   84   final ClipboardStatusNotifier clipboardStatus;
   85   final VoidCallback handleCut;
   86   final VoidCallback handleCopy;
   87   final VoidCallback handlePaste;
   88   final VoidCallback handleSelectAll;
   89   final bool isArrowPointingDown;
   90 
   91   @override
   92   _CupertinoTextSelectionToolbarWrapperState createState() => _CupertinoTextSelectionToolbarWrapperState();
   93 }
   94 
   95 class _CupertinoTextSelectionToolbarWrapperState extends State<_CupertinoTextSelectionToolbarWrapper> {
   96   ClipboardStatusNotifier _clipboardStatus;
   97 
   98   void _onChangedClipboardStatus() {
   99     setState(() {
  100       // Inform the widget that the value of clipboardStatus has changed.
  101     });
  102   }
  103 
  104   @override
  105   void initState() {
  106     super.initState();
  107     _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier();
  108     _clipboardStatus.addListener(_onChangedClipboardStatus);
  109     _clipboardStatus.update();
  110   }
  111 
  112   @override
  113   void didUpdateWidget(_CupertinoTextSelectionToolbarWrapper oldWidget) {
  114     super.didUpdateWidget(oldWidget);
  115     if (oldWidget.clipboardStatus == null && widget.clipboardStatus != null) {
  116       _clipboardStatus.removeListener(_onChangedClipboardStatus);
  117       _clipboardStatus.dispose();
  118       _clipboardStatus = widget.clipboardStatus;
  119     } else if (oldWidget.clipboardStatus != null) {
  120       if (widget.clipboardStatus == null) {
  121         _clipboardStatus = ClipboardStatusNotifier();
  122         _clipboardStatus.addListener(_onChangedClipboardStatus);
  123         oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
  124       } else if (widget.clipboardStatus != oldWidget.clipboardStatus) {
  125         _clipboardStatus = widget.clipboardStatus;
  126         _clipboardStatus.addListener(_onChangedClipboardStatus);
  127         oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
  128       }
  129     }
  130     if (widget.handlePaste != null) {
  131       _clipboardStatus.update();
  132     }
  133   }
  134 
  135   @override
  136   void dispose() {
  137     super.dispose();
  138     // When used in an Overlay, this can be disposed after its creator has
  139     // already disposed _clipboardStatus.
  140     if (!_clipboardStatus.disposed) {
  141       _clipboardStatus.removeListener(_onChangedClipboardStatus);
  142       if (widget.clipboardStatus == null) {
  143         _clipboardStatus.dispose();
  144       }
  145     }
  146   }
  147 
  148   @override
  149   Widget build(BuildContext context) {
  150     // Don't render the menu until the state of the clipboard is known.
  151     if (widget.handlePaste != null
  152         && _clipboardStatus.value == ClipboardStatus.unknown) {
  153       return const SizedBox(width: 0.0, height: 0.0);
  154     }
  155 
  156     final List<Widget> items = <Widget>[];
  157     final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
  158     final EdgeInsets arrowPadding = widget.isArrowPointingDown
  159       ? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
  160       : EdgeInsets.only(top: _kToolbarArrowSize.height);
  161     final Widget onePhysicalPixelVerticalDivider =
  162         SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
  163 
  164     void addToolbarButton(
  165       String text,
  166       VoidCallback onPressed,
  167     ) {
  168       if (items.isNotEmpty) {
  169         items.add(onePhysicalPixelVerticalDivider);
  170       }
  171 
  172       items.add(CupertinoButton(
  173         child: Text(
  174           text,
  175           overflow: TextOverflow.ellipsis,
  176           style: _kToolbarButtonFontStyle,
  177         ),
  178         borderRadius: null,
  179         color: _kToolbarBackgroundColor,
  180         minSize: _kToolbarHeight,
  181         onPressed: onPressed,
  182         padding: _kToolbarButtonPadding.add(arrowPadding),
  183         pressedOpacity: 0.7,
  184       ));
  185     }
  186 
  187     if (widget.handleCut != null) {
  188       addToolbarButton(localizations.cutButtonLabel, widget.handleCut);
  189     }
  190     if (widget.handleCopy != null) {
  191       addToolbarButton(localizations.copyButtonLabel, widget.handleCopy);
  192     }
  193     if (widget.handlePaste != null
  194         && _clipboardStatus.value == ClipboardStatus.pasteable) {
  195       addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste);
  196     }
  197     if (widget.handleSelectAll != null) {
  198       addToolbarButton(localizations.selectAllButtonLabel, widget.handleSelectAll);
  199     }
  200 
  201     return CupertinoTextSelectionToolbar._(
  202       barTopY: widget.barTopY,
  203       arrowTipX: widget.arrowTipX,
  204       isArrowPointingDown: widget.isArrowPointingDown,
  205       child: items.isEmpty ? null : _CupertinoTextSelectionToolbarContent(
  206         isArrowPointingDown: widget.isArrowPointingDown,
  207         children: items,
  208       ),
  209     );
  210   }
  211 }
  212 
  213 /// An iOS-style toolbar that appears in response to text selection.
  214 ///
  215 /// Typically displays buttons for text manipulation, e.g. copying and pasting text.
  216 ///
  217 /// See also:
  218 ///
  219 ///  * [TextSelectionControls.buildToolbar], where [CupertinoTextSelectionToolbar]
  220 ///    will be used to build an iOS-style toolbar.
  221 @visibleForTesting
  222 class CupertinoTextSelectionToolbar extends SingleChildRenderObjectWidget {
  223   const CupertinoTextSelectionToolbar._({
  224     Key key,
  225     double barTopY,
  226     double arrowTipX,
  227     bool isArrowPointingDown,
  228     Widget child,
  229   }) : _barTopY = barTopY,
  230        _arrowTipX = arrowTipX,
  231        _isArrowPointingDown = isArrowPointingDown,
  232        super(key: key, child: child);
  233 
  234   // The y-coordinate of toolbar's top edge, in global coordinate system.
  235   final double _barTopY;
  236 
  237   // The y-coordinate of the tip of the arrow, in global coordinate system.
  238   final double _arrowTipX;
  239 
  240   // Whether the arrow should point down and be attached to the bottom
  241   // of the toolbar, or point up and be attached to the top of the toolbar.
  242   final bool _isArrowPointingDown;
  243 
  244   @override
  245   _ToolbarRenderBox createRenderObject(BuildContext context) => _ToolbarRenderBox(_barTopY, _arrowTipX, _isArrowPointingDown, null);
  246 
  247   @override
  248   void updateRenderObject(BuildContext context, _ToolbarRenderBox renderObject) {
  249     renderObject
  250       ..barTopY = _barTopY
  251       ..arrowTipX = _arrowTipX
  252       ..isArrowPointingDown = _isArrowPointingDown;
  253   }
  254 }
  255 
  256 class _ToolbarParentData extends BoxParentData {
  257   // The x offset from the tip of the arrow to the center of the toolbar.
  258   // Positive if the tip of the arrow has a larger x-coordinate than the
  259   // center of the toolbar.
  260   double arrowXOffsetFromCenter;
  261   @override
  262   String toString() => 'offset=$offset, arrowXOffsetFromCenter=$arrowXOffsetFromCenter';
  263 }
  264 
  265 class _ToolbarRenderBox extends RenderShiftedBox {
  266   _ToolbarRenderBox(
  267     this._barTopY,
  268     this._arrowTipX,
  269     this._isArrowPointingDown,
  270     RenderBox child,
  271   ) : super(child);
  272 
  273 
  274   @override
  275   bool get isRepaintBoundary => true;
  276 
  277   double _barTopY;
  278   set barTopY(double value) {
  279     if (_barTopY == value) {
  280       return;
  281     }
  282     _barTopY = value;
  283     markNeedsLayout();
  284     markNeedsSemanticsUpdate();
  285   }
  286 
  287   double _arrowTipX;
  288   set arrowTipX(double value) {
  289     if (_arrowTipX == value) {
  290       return;
  291     }
  292     _arrowTipX = value;
  293     markNeedsLayout();
  294     markNeedsSemanticsUpdate();
  295   }
  296 
  297   bool _isArrowPointingDown;
  298   set isArrowPointingDown(bool value) {
  299     if (_isArrowPointingDown == value) {
  300       return;
  301     }
  302     _isArrowPointingDown = value;
  303     markNeedsLayout();
  304     markNeedsSemanticsUpdate();
  305   }
  306 
  307   final BoxConstraints heightConstraint = const BoxConstraints.tightFor(height: _kToolbarHeight);
  308 
  309   @override
  310   void setupParentData(RenderObject child) {
  311     if (child.parentData is! _ToolbarParentData) {
  312       child.parentData = _ToolbarParentData();
  313     }
  314   }
  315 
  316   @override
  317   void performLayout() {
  318     final BoxConstraints constraints = this.constraints;
  319     size = constraints.biggest;
  320 
  321     if (child == null) {
  322       return;
  323     }
  324     final BoxConstraints enforcedConstraint = constraints
  325       .deflate(const EdgeInsets.symmetric(horizontal: _kToolbarScreenPadding))
  326       .loosen();
  327 
  328     child.layout(heightConstraint.enforce(enforcedConstraint), parentUsesSize: true,);
  329     final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
  330 
  331     // The local x-coordinate of the center of the toolbar.
  332     final double lowerBound = child.size.width/2 + _kToolbarScreenPadding;
  333     final double upperBound = size.width - child.size.width/2 - _kToolbarScreenPadding;
  334     final double adjustedCenterX = _arrowTipX.clamp(lowerBound, upperBound) as double;
  335 
  336     childParentData.offset = Offset(adjustedCenterX - child.size.width / 2, _barTopY);
  337     childParentData.arrowXOffsetFromCenter = _arrowTipX - adjustedCenterX;
  338   }
  339 
  340   // The path is described in the toolbar's coordinate system.
  341   Path _clipPath() {
  342     final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
  343     final Path rrect = Path()
  344       ..addRRect(
  345         RRect.fromRectAndRadius(
  346           Offset(0, _isArrowPointingDown ? 0 : _kToolbarArrowSize.height,)
  347           & Size(child.size.width, child.size.height - _kToolbarArrowSize.height),
  348           _kToolbarBorderRadius,
  349         ),
  350       );
  351 
  352     final double arrowTipX = child.size.width / 2 + childParentData.arrowXOffsetFromCenter;
  353 
  354     final double arrowBottomY = _isArrowPointingDown
  355       ? child.size.height - _kToolbarArrowSize.height
  356       : _kToolbarArrowSize.height;
  357 
  358     final double arrowTipY = _isArrowPointingDown ? child.size.height : 0;
  359 
  360     final Path arrow = Path()
  361       ..moveTo(arrowTipX, arrowTipY)
  362       ..lineTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBottomY)
  363       ..lineTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBottomY)
  364       ..close();
  365 
  366     return Path.combine(PathOperation.union, rrect, arrow);
  367   }
  368 
  369   @override
  370   void paint(PaintingContext context, Offset offset) {
  371     if (child == null) {
  372       return;
  373     }
  374 
  375     final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
  376     context.pushClipPath(
  377       needsCompositing,
  378       offset + childParentData.offset,
  379       Offset.zero & child.size,
  380       _clipPath(),
  381       (PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child, innerOffset),
  382     );
  383   }
  384 
  385   Paint _debugPaint;
  386 
  387   @override
  388   void debugPaintSize(PaintingContext context, Offset offset) {
  389     assert(() {
  390       if (child == null) {
  391         return true;
  392       }
  393 
  394       _debugPaint ??= Paint()
  395       ..shader = ui.Gradient.linear(
  396         const Offset(0.0, 0.0),
  397         const Offset(10.0, 10.0),
  398         <Color>[const Color(0x00000000), const Color(0xFFFF00FF), const Color(0xFFFF00FF), const Color(0x00000000)],
  399         <double>[0.25, 0.25, 0.75, 0.75],
  400         TileMode.repeated,
  401       )
  402       ..strokeWidth = 2.0
  403       ..style = PaintingStyle.stroke;
  404 
  405       final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
  406       context.canvas.drawPath(_clipPath().shift(offset + childParentData.offset), _debugPaint);
  407       return true;
  408     }());
  409   }
  410 }
  411 
  412 /// Draws a single text selection handle with a bar and a ball.
  413 class _TextSelectionHandlePainter extends CustomPainter {
  414   const _TextSelectionHandlePainter(this.color);
  415 
  416   final Color color;
  417 
  418   @override
  419   void paint(Canvas canvas, Size size) {
  420     const double halfStrokeWidth = 1.0;
  421     final Paint paint = Paint()..color = color;
  422     final Rect circle = Rect.fromCircle(
  423       center: const Offset(_kSelectionHandleRadius, _kSelectionHandleRadius),
  424       radius: _kSelectionHandleRadius,
  425     );
  426     final Rect line = Rect.fromPoints(
  427       const Offset(
  428         _kSelectionHandleRadius - halfStrokeWidth,
  429         2 * _kSelectionHandleRadius - _kSelectionHandleOverlap,
  430       ),
  431       Offset(_kSelectionHandleRadius + halfStrokeWidth, size.height),
  432     );
  433     final Path path = Path()
  434       ..addOval(circle)
  435     // Draw line so it slightly overlaps the circle.
  436       ..addRect(line);
  437     canvas.drawPath(path, paint);
  438   }
  439 
  440   @override
  441   bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => color != oldPainter.color;
  442 }
  443 
  444 class _CupertinoTextSelectionControls extends TextSelectionControls {
  445   /// Returns the size of the Cupertino handle.
  446   @override
  447   Size getHandleSize(double textLineHeight) {
  448     return Size(
  449       _kSelectionHandleRadius * 2,
  450       textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap,
  451     );
  452   }
  453 
  454   /// Builder for iOS-style copy/paste text selection toolbar.
  455   @override
  456   Widget buildToolbar(
  457     BuildContext context,
  458     Rect globalEditableRegion,
  459     double textLineHeight,
  460     Offset position,
  461     List<TextSelectionPoint> endpoints,
  462     TextSelectionDelegate delegate,
  463     ClipboardStatusNotifier clipboardStatus,
  464   ) {
  465     assert(debugCheckHasMediaQuery(context));
  466     final MediaQueryData mediaQuery = MediaQuery.of(context);
  467 
  468     // The toolbar should appear below the TextField when there is not enough
  469     // space above the TextField to show it, assuming there's always enough space
  470     // at the bottom in this case.
  471     final double toolbarHeightNeeded = mediaQuery.padding.top
  472       + _kToolbarScreenPadding
  473       + _kToolbarHeight
  474       + _kToolbarContentDistance;
  475     final double availableHeight = globalEditableRegion.top + endpoints.first.point.dy - textLineHeight;
  476     final bool isArrowPointingDown = toolbarHeightNeeded <= availableHeight;
  477 
  478     final double arrowTipX = (position.dx + globalEditableRegion.left).clamp(
  479       _kArrowScreenPadding + mediaQuery.padding.left,
  480       mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
  481     ) as double;
  482 
  483     // The y-coordinate has to be calculated instead of directly quoting position.dy,
  484     // since the caller (TextSelectionOverlay._buildToolbar) does not know whether
  485     // the toolbar is going to be facing up or down.
  486     final double localBarTopY = isArrowPointingDown
  487       ? endpoints.first.point.dy - textLineHeight - _kToolbarContentDistance - _kToolbarHeight
  488       : endpoints.last.point.dy + _kToolbarContentDistance;
  489 
  490     return _CupertinoTextSelectionToolbarWrapper(
  491       arrowTipX: arrowTipX,
  492       barTopY: localBarTopY + globalEditableRegion.top,
  493       clipboardStatus: clipboardStatus,
  494       handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
  495       handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null,
  496       handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
  497       handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
  498       isArrowPointingDown: isArrowPointingDown,
  499     );
  500   }
  501 
  502   /// Builder for iOS text selection edges.
  503   @override
  504   Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
  505     // We want a size that's a vertical line the height of the text plus a 18.0
  506     // padding in every direction that will constitute the selection drag area.
  507     final Size desiredSize = getHandleSize(textLineHeight);
  508 
  509     final Widget handle = SizedBox.fromSize(
  510       size: desiredSize,
  511       child: CustomPaint(
  512         painter: _TextSelectionHandlePainter(CupertinoTheme.of(context).primaryColor),
  513       ),
  514     );
  515 
  516     // [buildHandle]'s widget is positioned at the selection cursor's bottom
  517     // baseline. We transform the handle such that the SizedBox is superimposed
  518     // on top of the text selection endpoints.
  519     switch (type) {
  520       case TextSelectionHandleType.left:
  521         return handle;
  522       case TextSelectionHandleType.right:
  523         // Right handle is a vertical mirror of the left.
  524         return Transform(
  525           transform: Matrix4.identity()
  526             ..translate(desiredSize.width / 2, desiredSize.height / 2)
  527             ..rotateZ(math.pi)
  528             ..translate(-desiredSize.width / 2, -desiredSize.height / 2),
  529           child: handle,
  530         );
  531       // iOS doesn't draw anything for collapsed selections.
  532       case TextSelectionHandleType.collapsed:
  533         return const SizedBox();
  534     }
  535     assert(type != null);
  536     return null;
  537   }
  538 
  539   /// Gets anchor for cupertino-style text selection handles.
  540   ///
  541   /// See [TextSelectionControls.getHandleAnchor].
  542   @override
  543   Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
  544     final Size handleSize = getHandleSize(textLineHeight);
  545     switch (type) {
  546       // The circle is at the top for the left handle, and the anchor point is
  547       // all the way at the bottom of the line.
  548       case TextSelectionHandleType.left:
  549         return Offset(
  550           handleSize.width / 2,
  551           handleSize.height,
  552         );
  553       // The right handle is vertically flipped, and the anchor point is near
  554       // the top of the circle to give slight overlap.
  555       case TextSelectionHandleType.right:
  556         return Offset(
  557           handleSize.width / 2,
  558           handleSize.height - 2 * _kSelectionHandleRadius + _kSelectionHandleOverlap,
  559         );
  560       // A collapsed handle anchors itself so that it's centered.
  561       default:
  562         return Offset(
  563           handleSize.width / 2,
  564           textLineHeight + (handleSize.height - textLineHeight) / 2,
  565         );
  566     }
  567   }
  568 }
  569 
  570 // Renders the content of the selection menu and maintains the page state.
  571 class _CupertinoTextSelectionToolbarContent extends StatefulWidget {
  572   const _CupertinoTextSelectionToolbarContent({
  573     Key key,
  574     @required this.children,
  575     @required this.isArrowPointingDown,
  576   }) : assert(children != null),
  577        // This ignore is used because .isNotEmpty isn't compatible with const.
  578        assert(children.length > 0), // ignore: prefer_is_empty
  579        super(key: key);
  580 
  581   final List<Widget> children;
  582   final bool isArrowPointingDown;
  583 
  584   @override
  585   _CupertinoTextSelectionToolbarContentState createState() => _CupertinoTextSelectionToolbarContentState();
  586 }
  587 
  588 class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSelectionToolbarContent> with TickerProviderStateMixin {
  589   // Controls the fading of the buttons within the menu during page transitions.
  590   AnimationController _controller;
  591   int _page = 0;
  592   int _nextPage;
  593 
  594   void _handleNextPage() {
  595     _controller.reverse();
  596     _controller.addStatusListener(_statusListener);
  597     _nextPage = _page + 1;
  598   }
  599 
  600   void _handlePreviousPage() {
  601     _controller.reverse();
  602     _controller.addStatusListener(_statusListener);
  603     _nextPage = _page - 1;
  604   }
  605 
  606   void _statusListener(AnimationStatus status) {
  607     if (status != AnimationStatus.dismissed) {
  608       return;
  609     }
  610 
  611     setState(() {
  612       _page = _nextPage;
  613       _nextPage = null;
  614     });
  615     _controller.forward();
  616     _controller.removeStatusListener(_statusListener);
  617   }
  618 
  619   @override
  620   void initState() {
  621     super.initState();
  622     _controller = AnimationController(
  623       value: 1.0,
  624       vsync: this,
  625       // This was eyeballed on a physical iOS device running iOS 13.
  626       duration: const Duration(milliseconds: 150),
  627     );
  628   }
  629 
  630   @override
  631   void didUpdateWidget(_CupertinoTextSelectionToolbarContent oldWidget) {
  632     // If the children are changing, the current page should be reset.
  633     if (widget.children != oldWidget.children) {
  634       _page = 0;
  635       _nextPage = null;
  636       _controller.forward();
  637       _controller.removeStatusListener(_statusListener);
  638     }
  639     super.didUpdateWidget(oldWidget);
  640   }
  641 
  642   @override
  643   void dispose() {
  644     _controller.dispose();
  645     super.dispose();
  646   }
  647 
  648   @override
  649   Widget build(BuildContext context) {
  650     final EdgeInsets arrowPadding = widget.isArrowPointingDown
  651       ? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
  652       : EdgeInsets.only(top: _kToolbarArrowSize.height);
  653 
  654     return DecoratedBox(
  655       decoration: const BoxDecoration(color: _kToolbarDividerColor),
  656       child: FadeTransition(
  657         opacity: _controller,
  658         child: _CupertinoTextSelectionToolbarItems(
  659           page: _page,
  660           backButton: CupertinoButton(
  661             borderRadius: null,
  662             color: _kToolbarBackgroundColor,
  663             minSize: _kToolbarHeight,
  664             onPressed: _handlePreviousPage,
  665             padding: arrowPadding,
  666             pressedOpacity: 0.7,
  667             child: const Text('◀', style: _kToolbarButtonFontStyle),
  668           ),
  669           dividerWidth: 1.0 / MediaQuery.of(context).devicePixelRatio,
  670           nextButton: CupertinoButton(
  671             borderRadius: null,
  672             color: _kToolbarBackgroundColor,
  673             minSize: _kToolbarHeight,
  674             onPressed: _handleNextPage,
  675             padding: arrowPadding,
  676             pressedOpacity: 0.7,
  677             child: const Text('▶', style: _kToolbarButtonFontStyle),
  678           ),
  679           nextButtonDisabled: CupertinoButton(
  680             borderRadius: null,
  681             color: _kToolbarBackgroundColor,
  682             disabledColor: _kToolbarBackgroundColor,
  683             minSize: _kToolbarHeight,
  684             onPressed: null,
  685             padding: arrowPadding,
  686             pressedOpacity: 1.0,
  687             child: const Text('▶', style: _kToolbarButtonDisabledFontStyle),
  688           ),
  689           children: widget.children,
  690         ),
  691       ),
  692     );
  693   }
  694 }
  695 
  696 // The custom RenderObjectWidget that, together with
  697 // _CupertinoTextSelectionToolbarItemsRenderBox and
  698 // _CupertinoTextSelectionToolbarItemsElement, paginates the menu items.
  699 class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget {
  700   _CupertinoTextSelectionToolbarItems({
  701     Key key,
  702     @required this.page,
  703     @required this.children,
  704     @required this.backButton,
  705     @required this.dividerWidth,
  706     @required this.nextButton,
  707     @required this.nextButtonDisabled,
  708   }) : assert(children != null),
  709        assert(children.isNotEmpty),
  710        assert(backButton != null),
  711        assert(dividerWidth != null),
  712        assert(nextButton != null),
  713        assert(nextButtonDisabled != null),
  714        assert(page != null),
  715        super(key: key);
  716 
  717   final Widget backButton;
  718   final List<Widget> children;
  719   final double dividerWidth;
  720   final Widget nextButton;
  721   final Widget nextButtonDisabled;
  722   final int page;
  723 
  724   @override
  725   _CupertinoTextSelectionToolbarItemsRenderBox createRenderObject(BuildContext context) {
  726     return _CupertinoTextSelectionToolbarItemsRenderBox(
  727       dividerWidth: dividerWidth,
  728       page: page,
  729     );
  730   }
  731 
  732   @override
  733   void updateRenderObject(BuildContext context, _CupertinoTextSelectionToolbarItemsRenderBox renderObject) {
  734     renderObject
  735       ..page = page
  736       ..dividerWidth = dividerWidth;
  737   }
  738 
  739   @override
  740   _CupertinoTextSelectionToolbarItemsElement createElement() => _CupertinoTextSelectionToolbarItemsElement(this);
  741 }
  742 
  743 // The custom RenderObjectElement that helps paginate the menu items.
  744 class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
  745   _CupertinoTextSelectionToolbarItemsElement(
  746     _CupertinoTextSelectionToolbarItems widget,
  747   ) : super(widget);
  748 
  749   List<Element> _children;
  750   final Map<_CupertinoTextSelectionToolbarItemsSlot, Element> slotToChild = <_CupertinoTextSelectionToolbarItemsSlot, Element>{};
  751 
  752   // We keep a set of forgotten children to avoid O(n^2) work walking _children
  753   // repeatedly to remove children.
  754   final Set<Element> _forgottenChildren = HashSet<Element>();
  755 
  756   @override
  757   _CupertinoTextSelectionToolbarItems get widget => super.widget as _CupertinoTextSelectionToolbarItems;
  758 
  759   @override
  760   _CupertinoTextSelectionToolbarItemsRenderBox get renderObject => super.renderObject as _CupertinoTextSelectionToolbarItemsRenderBox;
  761 
  762   void _updateRenderObject(RenderBox child, _CupertinoTextSelectionToolbarItemsSlot slot) {
  763     switch (slot) {
  764       case _CupertinoTextSelectionToolbarItemsSlot.backButton:
  765         renderObject.backButton = child;
  766         break;
  767       case _CupertinoTextSelectionToolbarItemsSlot.nextButton:
  768         renderObject.nextButton = child;
  769         break;
  770       case _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled:
  771         renderObject.nextButtonDisabled = child;
  772         break;
  773     }
  774   }
  775 
  776   @override
  777   void insertRenderObjectChild(RenderObject child, dynamic slot) {
  778     if (slot is _CupertinoTextSelectionToolbarItemsSlot) {
  779       assert(child is RenderBox);
  780       _updateRenderObject(child as RenderBox, slot);
  781       assert(renderObject.slottedChildren.containsKey(slot));
  782       return;
  783     }
  784     if (slot is IndexedSlot) {
  785       assert(renderObject.debugValidateChild(child));
  786       renderObject.insert(child as RenderBox, after: slot?.value?.renderObject as RenderBox);
  787       return;
  788     }
  789     assert(false, 'slot must be _CupertinoTextSelectionToolbarItemsSlot or IndexedSlot');
  790   }
  791 
  792   // This is not reachable for children that don't have an IndexedSlot.
  793   @override
  794   void moveRenderObjectChild(RenderObject child, IndexedSlot<Element> oldSlot, IndexedSlot<Element> newSlot) {
  795     assert(child.parent == renderObject);
  796     renderObject.move(child as RenderBox, after: newSlot?.value?.renderObject as RenderBox);
  797   }
  798 
  799   static bool _shouldPaint(Element child) {
  800     return (child.renderObject.parentData as ToolbarItemsParentData).shouldPaint;
  801   }
  802 
  803   @override
  804   void removeRenderObjectChild(RenderObject child, dynamic slot) {
  805     // Check if the child is in a slot.
  806     if (slot is _CupertinoTextSelectionToolbarItemsSlot) {
  807       assert(child is RenderBox);
  808       assert(renderObject.slottedChildren.containsKey(slot));
  809       _updateRenderObject(null, slot);
  810       assert(!renderObject.slottedChildren.containsKey(slot));
  811       return;
  812     }
  813 
  814     // Otherwise look for it in the list of children.
  815     assert(slot is IndexedSlot);
  816     assert(child.parent == renderObject);
  817     renderObject.remove(child as RenderBox);
  818   }
  819 
  820   @override
  821   void visitChildren(ElementVisitor visitor) {
  822     slotToChild.values.forEach(visitor);
  823     for (final Element child in _children) {
  824       if (!_forgottenChildren.contains(child))
  825         visitor(child);
  826     }
  827   }
  828 
  829   @override
  830   void forgetChild(Element child) {
  831     assert(slotToChild.containsValue(child) || _children.contains(child));
  832     assert(!_forgottenChildren.contains(child));
  833     // Handle forgetting a child in children or in a slot.
  834     if (slotToChild.containsKey(child.slot)) {
  835       final _CupertinoTextSelectionToolbarItemsSlot slot = child.slot as _CupertinoTextSelectionToolbarItemsSlot;
  836       slotToChild.remove(slot);
  837     } else {
  838       _forgottenChildren.add(child);
  839     }
  840     super.forgetChild(child);
  841   }
  842 
  843   // Mount or update slotted child.
  844   void _mountChild(Widget widget, _CupertinoTextSelectionToolbarItemsSlot slot) {
  845     final Element oldChild = slotToChild[slot];
  846     final Element newChild = updateChild(oldChild, widget, slot);
  847     if (oldChild != null) {
  848       slotToChild.remove(slot);
  849     }
  850     if (newChild != null) {
  851       slotToChild[slot] = newChild;
  852     }
  853   }
  854 
  855   @override
  856   void mount(Element parent, dynamic newSlot) {
  857     super.mount(parent, newSlot);
  858     // Mount slotted children.
  859     _mountChild(widget.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
  860     _mountChild(widget.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
  861     _mountChild(widget.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
  862 
  863     // Mount list children.
  864     _children = List<Element>(widget.children.length);
  865     Element previousChild;
  866     for (int i = 0; i < _children.length; i += 1) {
  867       final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild));
  868       _children[i] = newChild;
  869       previousChild = newChild;
  870     }
  871   }
  872 
  873   @override
  874   void debugVisitOnstageChildren(ElementVisitor visitor) {
  875     // Visit slot children.
  876     for (final Element child in slotToChild.values) {
  877       if (_shouldPaint(child) && !_forgottenChildren.contains(child)) {
  878         visitor(child);
  879       }
  880     }
  881     // Visit list children.
  882     _children
  883         .where((Element child) => !_forgottenChildren.contains(child) && _shouldPaint(child))
  884         .forEach(visitor);
  885   }
  886 
  887   @override
  888   void update(_CupertinoTextSelectionToolbarItems newWidget) {
  889     super.update(newWidget);
  890     assert(widget == newWidget);
  891 
  892     // Update slotted children.
  893     _mountChild(widget.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
  894     _mountChild(widget.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
  895     _mountChild(widget.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
  896 
  897     // Update list children.
  898     _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
  899     _forgottenChildren.clear();
  900   }
  901 }
  902 
  903 // The custom RenderBox that helps paginate the menu items.
  904 class _CupertinoTextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData>, RenderBoxContainerDefaultsMixin<RenderBox, ToolbarItemsParentData> {
  905   _CupertinoTextSelectionToolbarItemsRenderBox({
  906     @required double dividerWidth,
  907     @required int page,
  908   }) : assert(dividerWidth != null),
  909        assert(page != null),
  910        _dividerWidth = dividerWidth,
  911        _page = page,
  912        super();
  913 
  914   final Map<_CupertinoTextSelectionToolbarItemsSlot, RenderBox> slottedChildren = <_CupertinoTextSelectionToolbarItemsSlot, RenderBox>{};
  915 
  916   RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _CupertinoTextSelectionToolbarItemsSlot slot) {
  917     if (oldChild != null) {
  918       dropChild(oldChild);
  919       slottedChildren.remove(slot);
  920     }
  921     if (newChild != null) {
  922       slottedChildren[slot] = newChild;
  923       adoptChild(newChild);
  924     }
  925     return newChild;
  926   }
  927 
  928   bool _isSlottedChild(RenderBox child) {
  929     return child == _backButton || child == _nextButton || child == _nextButtonDisabled;
  930   }
  931 
  932   int _page;
  933   int get page => _page;
  934   set page(int value) {
  935     if (value == _page) {
  936       return;
  937     }
  938     _page = value;
  939     markNeedsLayout();
  940   }
  941 
  942   double _dividerWidth;
  943   double get dividerWidth => _dividerWidth;
  944   set dividerWidth(double value) {
  945     if (value == _dividerWidth) {
  946       return;
  947     }
  948     _dividerWidth = value;
  949     markNeedsLayout();
  950   }
  951 
  952   RenderBox _backButton;
  953   RenderBox get backButton => _backButton;
  954   set backButton(RenderBox value) {
  955     _backButton = _updateChild(_backButton, value, _CupertinoTextSelectionToolbarItemsSlot.backButton);
  956   }
  957 
  958   RenderBox _nextButton;
  959   RenderBox get nextButton => _nextButton;
  960   set nextButton(RenderBox value) {
  961     _nextButton = _updateChild(_nextButton, value, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
  962   }
  963 
  964   RenderBox _nextButtonDisabled;
  965   RenderBox get nextButtonDisabled => _nextButtonDisabled;
  966   set nextButtonDisabled(RenderBox value) {
  967     _nextButtonDisabled = _updateChild(_nextButtonDisabled, value, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
  968   }
  969 
  970   @override
  971   void performLayout() {
  972     if (firstChild == null) {
  973       performResize();
  974       return;
  975     }
  976 
  977     // Layout slotted children.
  978     _backButton.layout(constraints.loosen(), parentUsesSize: true);
  979     _nextButton.layout(constraints.loosen(), parentUsesSize: true);
  980     _nextButtonDisabled.layout(constraints.loosen(), parentUsesSize: true);
  981 
  982     final double subsequentPageButtonsWidth =
  983         _backButton.size.width + _nextButton.size.width;
  984     double currentButtonPosition = 0.0;
  985     double toolbarWidth; // The width of the whole widget.
  986     double firstPageWidth;
  987     int currentPage = 0;
  988     int i = -1;
  989     visitChildren((RenderObject renderObjectChild) {
  990       i++;
  991       final RenderBox child = renderObjectChild as RenderBox;
  992       final ToolbarItemsParentData childParentData =
  993           child.parentData as ToolbarItemsParentData;
  994       childParentData.shouldPaint = false;
  995 
  996       // Skip slotted children and children on pages after the visible page.
  997       if (_isSlottedChild(child) || currentPage > _page) {
  998         return;
  999       }
 1000 
 1001       double paginationButtonsWidth = 0.0;
 1002       if (currentPage == 0) {
 1003         // If this is the last child, it's ok to fit without a forward button.
 1004         paginationButtonsWidth =
 1005             i == childCount - 1 ? 0.0 : _nextButton.size.width;
 1006       } else {
 1007         paginationButtonsWidth = subsequentPageButtonsWidth;
 1008       }
 1009 
 1010       // The width of the menu is set by the first page.
 1011       child.layout(
 1012         BoxConstraints.loose(Size(
 1013           (currentPage == 0 ? constraints.maxWidth : firstPageWidth) - paginationButtonsWidth,
 1014           constraints.maxHeight,
 1015         )),
 1016         parentUsesSize: true,
 1017       );
 1018 
 1019       // If this child causes the current page to overflow, move to the next
 1020       // page and relayout the child.
 1021       final double currentWidth =
 1022           currentButtonPosition + paginationButtonsWidth + child.size.width;
 1023       if (currentWidth > constraints.maxWidth) {
 1024         currentPage++;
 1025         currentButtonPosition = _backButton.size.width + dividerWidth;
 1026         paginationButtonsWidth = _backButton.size.width + _nextButton.size.width;
 1027         child.layout(
 1028           BoxConstraints.loose(Size(
 1029             firstPageWidth - paginationButtonsWidth,
 1030             constraints.maxHeight,
 1031           )),
 1032           parentUsesSize: true,
 1033         );
 1034       }
 1035       childParentData.offset = Offset(currentButtonPosition, 0.0);
 1036       currentButtonPosition += child.size.width + dividerWidth;
 1037       childParentData.shouldPaint = currentPage == page;
 1038 
 1039       if (currentPage == 0) {
 1040         firstPageWidth = currentButtonPosition + _nextButton.size.width;
 1041       }
 1042       if (currentPage == page) {
 1043         toolbarWidth = currentButtonPosition;
 1044       }
 1045     });
 1046 
 1047     // It shouldn't be possible to navigate beyond the last page.
 1048     assert(page <= currentPage);
 1049 
 1050     // Position page nav buttons.
 1051     if (currentPage > 0) {
 1052       final ToolbarItemsParentData nextButtonParentData =
 1053           _nextButton.parentData as ToolbarItemsParentData;
 1054       final ToolbarItemsParentData nextButtonDisabledParentData =
 1055           _nextButtonDisabled.parentData as ToolbarItemsParentData;
 1056       final ToolbarItemsParentData backButtonParentData =
 1057           _backButton.parentData as ToolbarItemsParentData;
 1058       // The forward button always shows if there is more than one page, even on
 1059       // the last page (it's just disabled).
 1060       if (page == currentPage) {
 1061         nextButtonDisabledParentData.offset = Offset(toolbarWidth, 0.0);
 1062         nextButtonDisabledParentData.shouldPaint = true;
 1063         toolbarWidth += nextButtonDisabled.size.width;
 1064       } else {
 1065         nextButtonParentData.offset = Offset(toolbarWidth, 0.0);
 1066         nextButtonParentData.shouldPaint = true;
 1067         toolbarWidth += nextButton.size.width;
 1068       }
 1069       if (page > 0) {
 1070         backButtonParentData.offset = Offset.zero;
 1071         backButtonParentData.shouldPaint = true;
 1072         // No need to add the width of the back button to toolbarWidth here. It's
 1073         // already been taken care of when laying out the children to
 1074         // accommodate the back button.
 1075       }
 1076     } else {
 1077       // No divider for the next button when there's only one page.
 1078       toolbarWidth -= dividerWidth;
 1079     }
 1080 
 1081     size = constraints.constrain(Size(toolbarWidth, _kToolbarHeight));
 1082   }
 1083 
 1084   @override
 1085   void paint(PaintingContext context, Offset offset) {
 1086     visitChildren((RenderObject renderObjectChild) {
 1087       final RenderBox child = renderObjectChild as RenderBox;
 1088       final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
 1089 
 1090       if (childParentData.shouldPaint) {
 1091         final Offset childOffset = childParentData.offset + offset;
 1092         context.paintChild(child, childOffset);
 1093       }
 1094     });
 1095   }
 1096 
 1097   @override
 1098   void setupParentData(RenderBox child) {
 1099     if (child.parentData is! ToolbarItemsParentData) {
 1100       child.parentData = ToolbarItemsParentData();
 1101     }
 1102   }
 1103 
 1104   // Returns true iff the single child is hit by the given position.
 1105   static bool hitTestChild(RenderBox child, BoxHitTestResult result, { Offset position }) {
 1106     if (child == null) {
 1107       return false;
 1108     }
 1109     final ToolbarItemsParentData childParentData =
 1110         child.parentData as ToolbarItemsParentData;
 1111     return result.addWithPaintOffset(
 1112       offset: childParentData.offset,
 1113       position: position,
 1114       hitTest: (BoxHitTestResult result, Offset transformed) {
 1115         assert(transformed == position - childParentData.offset);
 1116         return child.hitTest(result, position: transformed);
 1117       },
 1118     );
 1119   }
 1120 
 1121   @override
 1122   bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
 1123     // Hit test list children.
 1124     // The x, y parameters have the top left of the node's box as the origin.
 1125     RenderBox child = lastChild;
 1126     while (child != null) {
 1127       final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
 1128 
 1129       // Don't hit test children that aren't shown.
 1130       if (!childParentData.shouldPaint) {
 1131         child = childParentData.previousSibling;
 1132         continue;
 1133       }
 1134 
 1135       if (hitTestChild(child, result, position: position)) {
 1136         return true;
 1137       }
 1138       child = childParentData.previousSibling;
 1139     }
 1140 
 1141     // Hit test slot children.
 1142     if (hitTestChild(backButton, result, position: position)) {
 1143       return true;
 1144     }
 1145     if (hitTestChild(nextButton, result, position: position)) {
 1146       return true;
 1147     }
 1148     if (hitTestChild(nextButtonDisabled, result, position: position)) {
 1149       return true;
 1150     }
 1151 
 1152     return false;
 1153   }
 1154 
 1155   @override
 1156   void attach(PipelineOwner owner) {
 1157     // Attach list children.
 1158     super.attach(owner);
 1159 
 1160     // Attach slot children.
 1161     for (final RenderBox child in slottedChildren.values) {
 1162       child.attach(owner);
 1163     }
 1164   }
 1165 
 1166   @override
 1167   void detach() {
 1168     // Detach list children.
 1169     super.detach();
 1170 
 1171     // Detach slot children.
 1172     for (final RenderBox child in slottedChildren.values) {
 1173       child.detach();
 1174     }
 1175   }
 1176 
 1177   @override
 1178   void redepthChildren() {
 1179     visitChildren((RenderObject renderObjectChild) {
 1180       final RenderBox child = renderObjectChild as RenderBox;
 1181       redepthChild(child);
 1182     });
 1183   }
 1184 
 1185   @override
 1186   void visitChildren(RenderObjectVisitor visitor) {
 1187     // Visit the slotted children.
 1188     if (_backButton != null) {
 1189       visitor(_backButton);
 1190     }
 1191     if (_nextButton != null) {
 1192       visitor(_nextButton);
 1193     }
 1194     if (_nextButtonDisabled != null) {
 1195       visitor(_nextButtonDisabled);
 1196     }
 1197     // Visit the list children.
 1198     super.visitChildren(visitor);
 1199   }
 1200 
 1201   // Visit only the children that should be painted.
 1202   @override
 1203   void visitChildrenForSemantics(RenderObjectVisitor visitor) {
 1204     visitChildren((RenderObject renderObjectChild) {
 1205       final RenderBox child = renderObjectChild as RenderBox;
 1206       final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
 1207       if (childParentData.shouldPaint) {
 1208         visitor(renderObjectChild);
 1209       }
 1210     });
 1211   }
 1212 
 1213   @override
 1214   List<DiagnosticsNode> debugDescribeChildren() {
 1215     final List<DiagnosticsNode> value = <DiagnosticsNode>[];
 1216     visitChildren((RenderObject renderObjectChild) {
 1217       if (renderObjectChild == null) {
 1218         return;
 1219       }
 1220       final RenderBox child = renderObjectChild as RenderBox;
 1221       if (child == backButton) {
 1222         value.add(child.toDiagnosticsNode(name: 'back button'));
 1223       } else if (child == nextButton) {
 1224         value.add(child.toDiagnosticsNode(name: 'next button'));
 1225       } else if (child == nextButtonDisabled) {
 1226         value.add(child.toDiagnosticsNode(name: 'next button disabled'));
 1227 
 1228       // List children.
 1229       } else {
 1230         value.add(child.toDiagnosticsNode(name: 'menu item'));
 1231       }
 1232     });
 1233     return value;
 1234   }
 1235 }
 1236 
 1237 // The slots that can be occupied by widgets in
 1238 // _CupertinoTextSelectionToolbarItems, excluding the list of children.
 1239 enum _CupertinoTextSelectionToolbarItemsSlot {
 1240   backButton,
 1241   nextButton,
 1242   nextButtonDisabled,
 1243 }
 1244 
 1245 /// Text selection controls that follows iOS design conventions.
 1246 final TextSelectionControls cupertinoTextSelectionControls = _CupertinoTextSelectionControls();