LCOV - code coverage report
Current view: top level - editing/text_selection - mongol_text_selection_toolbar.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 153 221 69.2 %
Date: 2021-07-30 09:13:58 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors.
       2             : // Copyright 2021 Suragch.
       3             : // All rights reserved.
       4             : // Use of this source code is governed by a BSD-style license that can be
       5             : // found in the LICENSE file.
       6             : 
       7             : import 'dart:math' as math;
       8             : 
       9             : import 'package:flutter/foundation.dart' show listEquals;
      10             : import 'package:flutter/material.dart' show Icons, Material, MaterialType, IconButton;
      11             : import 'package:flutter/rendering.dart';
      12             : import 'package:flutter/widgets.dart';
      13             : 
      14             : import 'mongol_text_selection_toolbar_layout_delegate.dart';
      15             : 
      16             : // Minimal padding from all edges of the selection toolbar to all edges of the
      17             : // viewport.
      18             : const double _kToolbarScreenPadding = 8.0;
      19             : const double _kToolbarWidth = 44.0;
      20             : 
      21             : /// A fully-functional Material-style text selection toolbar.
      22             : ///
      23             : /// Tries to position itself to the left of [anchorLeft], but if it doesn't fit,
      24             : /// then it positions itself to the right of [anchorRight].
      25             : ///
      26             : /// If any children don't fit in the menu, an overflow menu will automatically
      27             : /// be created.
      28             : ///
      29             : /// See also:
      30             : ///
      31             : ///  * [MongolTextSelectionControls.buildToolbar], where this is used by default to
      32             : ///    build an Android-style toolbar.
      33             : class MongolTextSelectionToolbar extends StatelessWidget {
      34             :   /// Creates an instance of MongolTextSelectionToolbar.
      35           1 :   const MongolTextSelectionToolbar({
      36             :     Key? key,
      37             :     required this.anchorLeft,
      38             :     required this.anchorRight,
      39             :     this.toolbarBuilder = _defaultToolbarBuilder,
      40             :     required this.children,
      41           2 :   })   : assert(children.length > 0),
      42           1 :         super(key: key);
      43             : 
      44             :   /// The focal point to the left of which the toolbar attempts to position
      45             :   /// itself.
      46             :   ///
      47             :   /// If there is not enough room to the left before reaching the left of the
      48             :   /// screen, then the toolbar will position itself to the right of
      49             :   /// [anchorRight].
      50             :   final Offset anchorLeft;
      51             : 
      52             :   /// The focal point to the right of which the toolbar attempts to position
      53             :   /// itself, if it doesn't fit to the left of [anchorLeft].
      54             :   final Offset anchorRight;
      55             : 
      56             :   /// The children that will be displayed in the text selection toolbar.
      57             :   ///
      58             :   /// Typically these are buttons.
      59             :   ///
      60             :   /// Must not be empty.
      61             :   ///
      62             :   /// See also:
      63             :   ///   * [MongolTextSelectionToolbarButton], which builds a toolbar button.
      64             :   final List<Widget> children;
      65             : 
      66             :   /// Builds the toolbar container.
      67             :   ///
      68             :   /// Useful for customizing the high-level background of the toolbar. The given
      69             :   /// child Widget will contain all of the [children].
      70             :   final ToolbarBuilder toolbarBuilder;
      71             : 
      72             :   // Build the default text selection menu toolbar.
      73           1 :   static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
      74           1 :     return _TextSelectionToolbarContainer(
      75             :       child: child,
      76             :     );
      77             :   }
      78             : 
      79           1 :   @override
      80             :   Widget build(BuildContext context) {
      81             :     final paddingLeft =
      82           4 :         MediaQuery.of(context).padding.left + _kToolbarScreenPadding;
      83           3 :     final availableWidth = anchorLeft.dx - paddingLeft;
      84           1 :     final fitsLeft = _kToolbarWidth <= availableWidth;
      85           1 :     final localAdjustment = Offset(paddingLeft, _kToolbarScreenPadding);
      86             : 
      87           1 :     return Padding(
      88           1 :       padding: EdgeInsets.fromLTRB(
      89             :         paddingLeft,
      90             :         _kToolbarScreenPadding,
      91             :         _kToolbarScreenPadding,
      92             :         _kToolbarScreenPadding,
      93             :       ),
      94           1 :       child: Stack(
      95           1 :         children: <Widget>[
      96           1 :           CustomSingleChildLayout(
      97           1 :             delegate: MongolTextSelectionToolbarLayoutDelegate(
      98           2 :               anchorLeft: anchorLeft - localAdjustment,
      99           2 :               anchorRight: anchorRight - localAdjustment,
     100             :               fitsLeft: fitsLeft,
     101             :             ),
     102           1 :             child: _TextSelectionToolbarOverflowable(
     103             :               isLeft: fitsLeft,
     104           1 :               toolbarBuilder: toolbarBuilder,
     105           1 :               children: children,
     106             :             ),
     107             :           ),
     108             :         ],
     109             :       ),
     110             :     );
     111             :   }
     112             : }
     113             : 
     114             : // A toolbar containing the given children. If they overflow the height
     115             : // available, then the overflowing children will be displayed in an overflow
     116             : // menu.
     117             : class _TextSelectionToolbarOverflowable extends StatefulWidget {
     118           1 :   const _TextSelectionToolbarOverflowable({
     119             :     Key? key,
     120             :     required this.isLeft,
     121             :     required this.toolbarBuilder,
     122             :     required this.children,
     123           2 :   })   : assert(children.length > 0),
     124           1 :         super(key: key);
     125             : 
     126             :   final List<Widget> children;
     127             : 
     128             :   // When true, the toolbar fits to the left of its anchor and will be
     129             :   // positioned there.
     130             :   final bool isLeft;
     131             : 
     132             :   // Builds the toolbar that will be populated with the children and fit inside
     133             :   // of the layout that adjusts to overflow.
     134             :   final ToolbarBuilder toolbarBuilder;
     135             : 
     136           1 :   @override
     137             :   _TextSelectionToolbarOverflowableState createState() =>
     138           1 :       _TextSelectionToolbarOverflowableState();
     139             : }
     140             : 
     141             : class _TextSelectionToolbarOverflowableState
     142             :     extends State<_TextSelectionToolbarOverflowable>
     143             :     with TickerProviderStateMixin {
     144             :   // Whether or not the overflow menu is open. When it is closed, the menu
     145             :   // items that don't overflow are shown. When it is open, only the overflowing
     146             :   // menu items are shown.
     147             :   bool _overflowOpen = false;
     148             : 
     149             :   // The key for _TextSelectionToolbarTrailingEdgeAlign.
     150             :   UniqueKey _containerKey = UniqueKey();
     151             : 
     152             :   // Close the menu and reset layout calculations, as in when the menu has
     153             :   // changed and saved values are no longer relevant. This should be called in
     154             :   // setState or another context where a rebuild is happening.
     155           0 :   void _reset() {
     156             :     // Change _TextSelectionToolbarTrailingEdgeAlign's key when the menu changes in
     157             :     // order to cause it to rebuild. This lets it recalculate its
     158             :     // saved height for the new set of children, and it prevents AnimatedSize
     159             :     // from animating the size change.
     160           0 :     _containerKey = UniqueKey();
     161             :     // If the menu items change, make sure the overflow menu is closed. This
     162             :     // prevents getting into a broken state where _overflowOpen is true when
     163             :     // there are not enough children to cause overflow.
     164           0 :     _overflowOpen = false;
     165             :   }
     166             : 
     167           0 :   @override
     168             :   void didUpdateWidget(_TextSelectionToolbarOverflowable oldWidget) {
     169           0 :     super.didUpdateWidget(oldWidget);
     170             :     // If the children are changing at all, the current page should be reset.
     171           0 :     if (!listEquals(widget.children, oldWidget.children)) {
     172           0 :       _reset();
     173             :     }
     174             :   }
     175             : 
     176           1 :   @override
     177             :   Widget build(BuildContext context) {
     178           1 :     return _TextSelectionToolbarTrailingEdgeAlign(
     179           1 :       key: _containerKey,
     180           1 :       overflowOpen: _overflowOpen,
     181           1 :       child: AnimatedSize(
     182             :         vsync: this,
     183             :         // This duration was eyeballed on a Pixel 2 emulator running Android
     184             :         // API 28.
     185             :         duration: const Duration(milliseconds: 140),
     186           3 :         child: widget.toolbarBuilder(
     187             :             context,
     188           1 :             _TextSelectionToolbarItemsLayout(
     189           2 :               isLeft: widget.isLeft,
     190           1 :               overflowOpen: _overflowOpen,
     191           1 :               children: <Widget>[
     192           1 :                 _TextSelectionToolbarOverflowButton(
     193             :                   icon:
     194           2 :                       Icon(_overflowOpen ? Icons.arrow_back : Icons.more_horiz),
     195           0 :                   onPressed: () {
     196           0 :                     setState(() {
     197           0 :                       _overflowOpen = !_overflowOpen;
     198             :                     });
     199             :                   },
     200             :                 ),
     201           2 :                 ...widget.children,
     202             :               ],
     203             :             )),
     204             :       ),
     205             :     );
     206             :   }
     207             : }
     208             : 
     209             : // When the overflow menu is open, it tries to align its trailing edge to the
     210             : // trailing edge of the closed menu. This widget handles this effect by
     211             : // measuring and maintaining the height of the closed menu and aligning the child
     212             : // to that side.
     213             : class _TextSelectionToolbarTrailingEdgeAlign
     214             :     extends SingleChildRenderObjectWidget {
     215           1 :   const _TextSelectionToolbarTrailingEdgeAlign({
     216             :     Key? key,
     217             :     required Widget child,
     218             :     required this.overflowOpen,
     219           1 :   }) : super(key: key, child: child);
     220             : 
     221             :   final bool overflowOpen;
     222             : 
     223           1 :   @override
     224             :   _TextSelectionToolbarTrailingEdgeAlignRenderBox createRenderObject(
     225             :       BuildContext context) {
     226           1 :     return _TextSelectionToolbarTrailingEdgeAlignRenderBox(
     227           1 :       overflowOpen: overflowOpen,
     228             :     );
     229             :   }
     230             : 
     231           0 :   @override
     232             :   void updateRenderObject(BuildContext context,
     233             :       _TextSelectionToolbarTrailingEdgeAlignRenderBox renderObject) {
     234           0 :     renderObject.overflowOpen = overflowOpen;
     235             :   }
     236             : }
     237             : 
     238             : class _TextSelectionToolbarTrailingEdgeAlignRenderBox extends RenderProxyBox {
     239           1 :   _TextSelectionToolbarTrailingEdgeAlignRenderBox({
     240             :     required bool overflowOpen,
     241             :   })   : _overflowOpen = overflowOpen,
     242           1 :         super();
     243             : 
     244             :   // The height of the menu when it was closed. This is used to achieve the
     245             :   // behavior where the open menu aligns its trailing edge to the closed menu's
     246             :   // trailing edge.
     247             :   double? _closedHeight;
     248             : 
     249             :   bool _overflowOpen;
     250           2 :   bool get overflowOpen => _overflowOpen;
     251           0 :   set overflowOpen(bool value) {
     252           0 :     if (value == overflowOpen) {
     253             :       return;
     254             :     }
     255           0 :     _overflowOpen = value;
     256           0 :     markNeedsLayout();
     257             :   }
     258             : 
     259           1 :   @override
     260             :   void performLayout() {
     261           4 :     child!.layout(constraints.loosen(), parentUsesSize: true);
     262             : 
     263             :     // Save the height when the menu is closed. If the menu changes, this height
     264             :     // is invalid, so it's important that this RenderBox be recreated in that
     265             :     // case. Currently, this is achieved by providing a new key to
     266             :     // _TextSelectionToolbarTrailingEdgeAlign.
     267           2 :     if (!overflowOpen && _closedHeight == null) {
     268           4 :       _closedHeight = child!.size.height;
     269             :     }
     270             : 
     271           4 :     size = constraints.constrain(Size(
     272           3 :       child!.size.width,
     273             :       // If the open menu is higher than the closed menu, just use its own height
     274             :       // and don't worry about aligning the trailing edges.
     275             :       // _closedHeight is used even when the menu is closed to allow it to
     276             :       // animate its size while keeping the same edge alignment.
     277           6 :       _closedHeight == null || child!.size.height > _closedHeight!
     278           0 :           ? child!.size.height
     279           1 :           : _closedHeight!,
     280             :     ));
     281             : 
     282             :     // Set the offset in the parent data such that the child will be aligned to
     283             :     // the trailing edge.
     284           2 :     final childParentData = child!.parentData! as ToolbarItemsParentData;
     285           2 :     childParentData.offset = Offset(
     286             :       0.0,
     287           6 :       size.height - child!.size.height,
     288             :     );
     289             :   }
     290             : 
     291             :   // Paint at the offset set in the parent data.
     292           1 :   @override
     293             :   void paint(PaintingContext context, Offset offset) {
     294           2 :     final childParentData = child!.parentData! as ToolbarItemsParentData;
     295           4 :     context.paintChild(child!, childParentData.offset + offset);
     296             :   }
     297             : 
     298             :   // Include the parent data offset in the hit test.
     299           0 :   @override
     300             :   bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
     301             :     // The x, y parameters have the top left of the node's box as the origin.
     302           0 :     final childParentData = child!.parentData! as ToolbarItemsParentData;
     303           0 :     return result.addWithPaintOffset(
     304           0 :       offset: childParentData.offset,
     305             :       position: position,
     306           0 :       hitTest: (BoxHitTestResult result, Offset transformed) {
     307           0 :         assert(transformed == position - childParentData.offset);
     308           0 :         return child!.hitTest(result, position: transformed);
     309             :       },
     310             :     );
     311             :   }
     312             : 
     313           1 :   @override
     314             :   void setupParentData(RenderBox child) {
     315           2 :     if (child.parentData is! ToolbarItemsParentData) {
     316           2 :       child.parentData = ToolbarItemsParentData();
     317             :     }
     318             :   }
     319             : 
     320           1 :   @override
     321             :   void applyPaintTransform(RenderObject child, Matrix4 transform) {
     322           1 :     final childParentData = child.parentData! as ToolbarItemsParentData;
     323           5 :     transform.translate(childParentData.offset.dx, childParentData.offset.dy);
     324           1 :     super.applyPaintTransform(child, transform);
     325             :   }
     326             : }
     327             : 
     328             : // Renders the menu items in the correct positions in the menu and its overflow
     329             : // submenu based on calculating which item would first overflow.
     330             : class _TextSelectionToolbarItemsLayout extends MultiChildRenderObjectWidget {
     331           1 :   _TextSelectionToolbarItemsLayout({
     332             :     Key? key,
     333             :     required this.isLeft,
     334             :     required this.overflowOpen,
     335             :     required List<Widget> children,
     336           1 :   }) : super(key: key, children: children);
     337             : 
     338             :   final bool isLeft;
     339             :   final bool overflowOpen;
     340             : 
     341           1 :   @override
     342             :   _RenderTextSelectionToolbarItemsLayout createRenderObject(
     343             :       BuildContext context) {
     344           1 :     return _RenderTextSelectionToolbarItemsLayout(
     345           1 :       isLeft: isLeft,
     346           1 :       overflowOpen: overflowOpen,
     347             :     );
     348             :   }
     349             : 
     350           0 :   @override
     351             :   void updateRenderObject(BuildContext context,
     352             :       _RenderTextSelectionToolbarItemsLayout renderObject) {
     353             :     renderObject
     354           0 :       ..isLeft = isLeft
     355           0 :       ..overflowOpen = overflowOpen;
     356             :   }
     357             : 
     358           1 :   @override
     359             :   _TextSelectionToolbarItemsLayoutElement createElement() =>
     360           1 :       _TextSelectionToolbarItemsLayoutElement(this);
     361             : }
     362             : 
     363             : class _TextSelectionToolbarItemsLayoutElement
     364             :     extends MultiChildRenderObjectElement {
     365           1 :   _TextSelectionToolbarItemsLayoutElement(
     366             :     MultiChildRenderObjectWidget widget,
     367           1 :   ) : super(widget);
     368             : 
     369           1 :   static bool _shouldPaint(Element child) {
     370           2 :     return (child.renderObject!.parentData! as ToolbarItemsParentData)
     371           1 :         .shouldPaint;
     372             :   }
     373             : 
     374           1 :   @override
     375             :   void debugVisitOnstageChildren(ElementVisitor visitor) {
     376           3 :     children.where(_shouldPaint).forEach(visitor);
     377             :   }
     378             : }
     379             : 
     380             : class _RenderTextSelectionToolbarItemsLayout extends RenderBox
     381             :     with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData> {
     382           1 :   _RenderTextSelectionToolbarItemsLayout({
     383             :     required bool isLeft,
     384             :     required bool overflowOpen,
     385             :   })   : _isLeft = isLeft,
     386             :         _overflowOpen = overflowOpen,
     387           1 :         super();
     388             : 
     389             :   // The index of the last item that doesn't overflow.
     390             :   int _lastIndexThatFits = -1;
     391             : 
     392             :   bool _isLeft;
     393           0 :   bool get isLeft => _isLeft;
     394           0 :   set isLeft(bool value) {
     395           0 :     if (value == isLeft) {
     396             :       return;
     397             :     }
     398           0 :     _isLeft = value;
     399           0 :     markNeedsLayout();
     400             :   }
     401             : 
     402             :   bool _overflowOpen;
     403           2 :   bool get overflowOpen => _overflowOpen;
     404           0 :   set overflowOpen(bool value) {
     405           0 :     if (value == overflowOpen) {
     406             :       return;
     407             :     }
     408           0 :     _overflowOpen = value;
     409           0 :     markNeedsLayout();
     410             :   }
     411             : 
     412             :   // Layout the necessary children, and figure out where the children first
     413             :   // overflow, if at all.
     414           1 :   void _layoutChildren() {
     415             :     // When overflow is not open, the toolbar is always a specific width.
     416           1 :     final sizedConstraints = _overflowOpen
     417           0 :         ? constraints
     418           2 :         : BoxConstraints.loose(Size(
     419             :             _kToolbarWidth,
     420           2 :             constraints.maxHeight,
     421             :           ));
     422             : 
     423           1 :     var i = -1;
     424             :     var height = 0.0;
     425           2 :     visitChildren((RenderObject renderObjectChild) {
     426           1 :       i++;
     427             : 
     428             :       // No need to layout children inside the overflow menu when it's closed.
     429             :       // The opposite is not true. It is necessary to layout the children that
     430             :       // don't overflow when the overflow menu is open in order to calculate
     431             :       // _lastIndexThatFits.
     432           3 :       if (_lastIndexThatFits != -1 && !overflowOpen) {
     433             :         return;
     434             :       }
     435             : 
     436             :       final child = renderObjectChild as RenderBox;
     437           2 :       child.layout(sizedConstraints.loosen(), parentUsesSize: true);
     438           3 :       height += child.size.height;
     439             : 
     440           2 :       if (height > sizedConstraints.maxHeight && _lastIndexThatFits == -1) {
     441           0 :         _lastIndexThatFits = i - 1;
     442             :       }
     443             :     });
     444             : 
     445             :     // If the last child overflows, but only because of the height of the
     446             :     // overflow button, then just show it and hide the overflow button.
     447           1 :     final navButton = firstChild!;
     448           3 :     if (_lastIndexThatFits != -1 &&
     449           0 :         _lastIndexThatFits == childCount - 2 &&
     450           0 :         height - navButton.size.height <= sizedConstraints.maxHeight) {
     451           0 :       _lastIndexThatFits = -1;
     452             :     }
     453             :   }
     454             : 
     455             :   // Returns true when the child should be painted, false otherwise.
     456           1 :   bool _shouldPaintChild(RenderObject renderObjectChild, int index) {
     457             :     // Paint the navButton when there is overflow.
     458           2 :     if (renderObjectChild == firstChild) {
     459           3 :       return _lastIndexThatFits != -1;
     460             :     }
     461             : 
     462             :     // If there is no overflow, all children besides the navButton are painted.
     463           3 :     if (_lastIndexThatFits == -1) {
     464             :       return true;
     465             :     }
     466             : 
     467             :     // When there is overflow, paint if the child is in the part of the menu
     468             :     // that is currently open. Overflowing children are painted when the
     469             :     // overflow menu is open, and the children that fit are painted when the
     470             :     // overflow menu is closed.
     471           0 :     return (index > _lastIndexThatFits) == overflowOpen;
     472             :   }
     473             : 
     474             :   // Decide which children will be painted, set their shouldPaint, and set the
     475             :   // offset that painted children will be placed at.
     476           1 :   void _placeChildren() {
     477           1 :     var i = -1;
     478             :     var nextSize = const Size(0.0, 0.0);
     479             :     var fitHeight = 0.0;
     480           1 :     final navButton = firstChild!;
     481           1 :     var overflowWidth = overflowOpen && !isLeft ? navButton.size.width : 0.0;
     482           2 :     visitChildren((RenderObject renderObjectChild) {
     483           1 :       i++;
     484             : 
     485             :       final child = renderObjectChild as RenderBox;
     486           1 :       final childParentData = child.parentData! as ToolbarItemsParentData;
     487             : 
     488             :       // Handle placing the navigation button after iterating all children.
     489           1 :       if (renderObjectChild == navButton) {
     490             :         return;
     491             :       }
     492             : 
     493             :       // There is no need to place children that won't be painted.
     494           1 :       if (!_shouldPaintChild(renderObjectChild, i)) {
     495           0 :         childParentData.shouldPaint = false;
     496             :         return;
     497             :       }
     498           1 :       childParentData.shouldPaint = true;
     499             : 
     500           1 :       if (!overflowOpen) {
     501           2 :         childParentData.offset = Offset(0.0, fitHeight);
     502           3 :         fitHeight += child.size.height;
     503           1 :         nextSize = Size(
     504           4 :           math.max(child.size.width, nextSize.width),
     505             :           fitHeight,
     506             :         );
     507             :       } else {
     508           0 :         childParentData.offset = Offset(overflowWidth, 0.0);
     509           0 :         overflowWidth += child.size.width;
     510           0 :         nextSize = Size(
     511             :           overflowWidth,
     512           0 :           math.max(child.size.height, nextSize.height),
     513             :         );
     514             :       }
     515             :     });
     516             : 
     517             :     // Place the navigation button if needed.
     518           1 :     final navButtonParentData = navButton.parentData! as ToolbarItemsParentData;
     519           2 :     if (_shouldPaintChild(firstChild!, 0)) {
     520           0 :       navButtonParentData.shouldPaint = true;
     521           0 :       if (overflowOpen) {
     522           0 :         navButtonParentData.offset =
     523           0 :             isLeft ? Offset(overflowWidth, 0.0) : Offset.zero;
     524           0 :         nextSize = Size(
     525           0 :           isLeft ? nextSize.width + navButton.size.width : nextSize.width,
     526           0 :           nextSize.height,
     527             :         );
     528             :       } else {
     529           0 :         navButtonParentData.offset = Offset(0.0, fitHeight);
     530             :         nextSize =
     531           0 :             Size(nextSize.width, nextSize.height + navButton.size.height);
     532             :       }
     533             :     } else {
     534           1 :       navButtonParentData.shouldPaint = false;
     535             :     }
     536             : 
     537           1 :     size = nextSize;
     538             :   }
     539             : 
     540           1 :   @override
     541             :   void performLayout() {
     542           2 :     _lastIndexThatFits = -1;
     543           1 :     if (firstChild == null) {
     544           0 :       size = constraints.smallest;
     545             :       return;
     546             :     }
     547             : 
     548           1 :     _layoutChildren();
     549           1 :     _placeChildren();
     550             :   }
     551             : 
     552           1 :   @override
     553             :   void paint(PaintingContext context, Offset offset) {
     554           2 :     visitChildren((RenderObject renderObjectChild) {
     555             :       final child = renderObjectChild as RenderBox;
     556           1 :       final childParentData = child.parentData! as ToolbarItemsParentData;
     557           1 :       if (!childParentData.shouldPaint) {
     558             :         return;
     559             :       }
     560             : 
     561           3 :       context.paintChild(child, childParentData.offset + offset);
     562             :     });
     563             :   }
     564             : 
     565           1 :   @override
     566             :   void setupParentData(RenderBox child) {
     567           2 :     if (child.parentData is! ToolbarItemsParentData) {
     568           2 :       child.parentData = ToolbarItemsParentData();
     569             :     }
     570             :   }
     571             : 
     572           0 :   @override
     573             :   bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
     574             :     // The x, y parameters have the top left of the node's box as the origin.
     575           0 :     var child = lastChild;
     576             :     while (child != null) {
     577           0 :       final childParentData = child.parentData! as ToolbarItemsParentData;
     578             : 
     579             :       // Don't hit test children aren't shown.
     580           0 :       if (!childParentData.shouldPaint) {
     581           0 :         child = childParentData.previousSibling;
     582             :         continue;
     583             :       }
     584             : 
     585           0 :       final isHit = result.addWithPaintOffset(
     586           0 :         offset: childParentData.offset,
     587             :         position: position,
     588           0 :         hitTest: (BoxHitTestResult result, Offset transformed) {
     589           0 :           assert(transformed == position - childParentData.offset);
     590           0 :           return child!.hitTest(result, position: transformed);
     591             :         },
     592             :       );
     593             :       if (isHit) {
     594             :         return true;
     595             :       }
     596           0 :       child = childParentData.previousSibling;
     597             :     }
     598             :     return false;
     599             :   }
     600             : 
     601             :   // Visit only the children that should be painted.
     602           1 :   @override
     603             :   void visitChildrenForSemantics(RenderObjectVisitor visitor) {
     604           2 :     visitChildren((RenderObject renderObjectChild) {
     605             :       final child = renderObjectChild as RenderBox;
     606             :       final childParentData =
     607           1 :           child.parentData! as ToolbarItemsParentData;
     608           1 :       if (childParentData.shouldPaint) {
     609           1 :         visitor(renderObjectChild);
     610             :       }
     611             :     });
     612             :   }
     613             : }
     614             : 
     615             : // The Material-styled toolbar outline. Fill it with any widgets you want. No
     616             : // overflow ability.
     617             : class _TextSelectionToolbarContainer extends StatelessWidget {
     618           1 :   const _TextSelectionToolbarContainer({
     619             :     Key? key,
     620             :     required this.child,
     621           1 :   }) : super(key: key);
     622             : 
     623             :   final Widget child;
     624             : 
     625           1 :   @override
     626             :   Widget build(BuildContext context) {
     627           1 :     return Material(
     628             :       // This value was eyeballed to match the native text selection menu on
     629             :       // a Pixel 2 running Android 10.
     630             :       borderRadius: const BorderRadius.all(Radius.circular(7.0)),
     631             :       clipBehavior: Clip.antiAlias,
     632             :       elevation: 1.0,
     633             :       type: MaterialType.card,
     634           1 :       child: child,
     635             :     );
     636             :   }
     637             : }
     638             : 
     639             : // A button styled like a Material native Android text selection overflow menu
     640             : // forward and back controls.
     641             : class _TextSelectionToolbarOverflowButton extends StatelessWidget {
     642           1 :   const _TextSelectionToolbarOverflowButton({
     643             :     Key? key,
     644             :     required this.icon,
     645             :     this.onPressed,
     646             :     this.tooltip,
     647           1 :   }) : super(key: key);
     648             : 
     649             :   final Icon icon;
     650             :   final VoidCallback? onPressed;
     651             :   final String? tooltip;
     652             : 
     653           1 :   @override
     654             :   Widget build(BuildContext context) {
     655           1 :     return Material(
     656             :       type: MaterialType.card,
     657             :       color: const Color(0x00000000),
     658           1 :       child: IconButton(
     659           1 :         icon: icon,
     660           1 :         onPressed: onPressed,
     661           1 :         tooltip: tooltip,
     662             :       ),
     663             :     );
     664             :   }
     665             : }

Generated by: LCOV version 1.15