704 lines
23 KiB
Dart
704 lines
23 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/foundation.dart' show listEquals;
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'debug.dart';
|
|
import 'icon_button.dart';
|
|
import 'icons.dart';
|
|
import 'material.dart';
|
|
import 'material_localizations.dart';
|
|
|
|
const double _kToolbarHeight = 44.0;
|
|
const double _kToolbarContentDistance = 8.0;
|
|
|
|
/// A fully-functional Material-style text selection toolbar.
|
|
///
|
|
/// Tries to position itself above [anchorAbove], but if it doesn't fit, then
|
|
/// it positions itself below [anchorBelow].
|
|
///
|
|
/// If any children don't fit in the menu, an overflow menu will automatically
|
|
/// be created.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [AdaptiveTextSelectionToolbar], which builds the toolbar for the current
|
|
/// platform.
|
|
/// * [CupertinoTextSelectionToolbar], which is similar, but builds an iOS-
|
|
/// style toolbar.
|
|
class TextSelectionToolbar extends StatelessWidget {
|
|
/// Creates an instance of TextSelectionToolbar.
|
|
const TextSelectionToolbar({
|
|
super.key,
|
|
required this.anchorAbove,
|
|
required this.anchorBelow,
|
|
this.toolbarBuilder = _defaultToolbarBuilder,
|
|
required this.children,
|
|
}) : assert(children.length > 0);
|
|
|
|
/// {@template flutter.material.TextSelectionToolbar.anchorAbove}
|
|
/// The focal point above which the toolbar attempts to position itself.
|
|
///
|
|
/// If there is not enough room above before reaching the top of the screen,
|
|
/// then the toolbar will position itself below [anchorBelow].
|
|
/// {@endtemplate}
|
|
final Offset anchorAbove;
|
|
|
|
/// {@template flutter.material.TextSelectionToolbar.anchorBelow}
|
|
/// The focal point below which the toolbar attempts to position itself, if it
|
|
/// doesn't fit above [anchorAbove].
|
|
/// {@endtemplate}
|
|
final Offset anchorBelow;
|
|
|
|
/// {@template flutter.material.TextSelectionToolbar.children}
|
|
/// The children that will be displayed in the text selection toolbar.
|
|
///
|
|
/// Typically these are buttons.
|
|
///
|
|
/// Must not be empty.
|
|
/// {@endtemplate}
|
|
///
|
|
/// See also:
|
|
/// * [TextSelectionToolbarTextButton], which builds a default Material-
|
|
/// style text selection toolbar text button.
|
|
final List<Widget> children;
|
|
|
|
/// {@template flutter.material.TextSelectionToolbar.toolbarBuilder}
|
|
/// Builds the toolbar container.
|
|
///
|
|
/// Useful for customizing the high-level background of the toolbar. The given
|
|
/// child Widget will contain all of the [children].
|
|
/// {@endtemplate}
|
|
final ToolbarBuilder toolbarBuilder;
|
|
|
|
/// Minimal padding from all edges of the selection toolbar to all edges of the
|
|
/// viewport.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SpellCheckSuggestionsToolbar], which uses this same value for its
|
|
/// padding from the edges of the viewport.
|
|
static const double kToolbarScreenPadding = 8.0;
|
|
|
|
/// The size of the text selection handles.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SpellCheckSuggestionsToolbar], which references this value to calculate
|
|
/// the padding between the toolbar and anchor.
|
|
static const double kHandleSize = 22.0;
|
|
|
|
/// Padding between the toolbar and the anchor.
|
|
static const double kToolbarContentDistanceBelow = kHandleSize - 2.0;
|
|
|
|
// Build the default Android Material text selection menu toolbar.
|
|
static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
|
|
return _TextSelectionToolbarContainer(
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Incorporate the padding distance between the content and toolbar.
|
|
final Offset anchorAbovePadded =
|
|
anchorAbove - const Offset(0.0, _kToolbarContentDistance);
|
|
final Offset anchorBelowPadded =
|
|
anchorBelow + const Offset(0.0, kToolbarContentDistanceBelow);
|
|
|
|
final double paddingAbove = MediaQuery.paddingOf(context).top
|
|
+ kToolbarScreenPadding;
|
|
final double availableHeight = anchorAbovePadded.dy - _kToolbarContentDistance - paddingAbove;
|
|
final bool fitsAbove = _kToolbarHeight <= availableHeight;
|
|
// Makes up for the Padding above the Stack.
|
|
final Offset localAdjustment = Offset(kToolbarScreenPadding, paddingAbove);
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
kToolbarScreenPadding,
|
|
paddingAbove,
|
|
kToolbarScreenPadding,
|
|
kToolbarScreenPadding,
|
|
),
|
|
child: CustomSingleChildLayout(
|
|
delegate: TextSelectionToolbarLayoutDelegate(
|
|
anchorAbove: anchorAbovePadded - localAdjustment,
|
|
anchorBelow: anchorBelowPadded - localAdjustment,
|
|
fitsAbove: fitsAbove,
|
|
),
|
|
child: _TextSelectionToolbarOverflowable(
|
|
isAbove: fitsAbove,
|
|
toolbarBuilder: toolbarBuilder,
|
|
children: children,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// A toolbar containing the given children. If they overflow the width
|
|
// available, then the overflowing children will be displayed in an overflow
|
|
// menu.
|
|
class _TextSelectionToolbarOverflowable extends StatefulWidget {
|
|
const _TextSelectionToolbarOverflowable({
|
|
required this.isAbove,
|
|
required this.toolbarBuilder,
|
|
required this.children,
|
|
}) : assert(children.length > 0);
|
|
|
|
final List<Widget> children;
|
|
|
|
// When true, the toolbar fits above its anchor and will be positioned there.
|
|
final bool isAbove;
|
|
|
|
// Builds the toolbar that will be populated with the children and fit inside
|
|
// of the layout that adjusts to overflow.
|
|
final ToolbarBuilder toolbarBuilder;
|
|
|
|
@override
|
|
_TextSelectionToolbarOverflowableState createState() => _TextSelectionToolbarOverflowableState();
|
|
}
|
|
|
|
class _TextSelectionToolbarOverflowableState extends State<_TextSelectionToolbarOverflowable> with TickerProviderStateMixin {
|
|
// Whether or not the overflow menu is open. When it is closed, the menu
|
|
// items that don't overflow are shown. When it is open, only the overflowing
|
|
// menu items are shown.
|
|
bool _overflowOpen = false;
|
|
|
|
// The key for _TextSelectionToolbarTrailingEdgeAlign.
|
|
UniqueKey _containerKey = UniqueKey();
|
|
|
|
// Close the menu and reset layout calculations, as in when the menu has
|
|
// changed and saved values are no longer relevant. This should be called in
|
|
// setState or another context where a rebuild is happening.
|
|
void _reset() {
|
|
// Change _TextSelectionToolbarTrailingEdgeAlign's key when the menu changes
|
|
// in order to cause it to rebuild. This lets it recalculate its
|
|
// saved width for the new set of children, and it prevents AnimatedSize
|
|
// from animating the size change.
|
|
_containerKey = UniqueKey();
|
|
// If the menu items change, make sure the overflow menu is closed. This
|
|
// prevents getting into a broken state where _overflowOpen is true when
|
|
// there are not enough children to cause overflow.
|
|
_overflowOpen = false;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_TextSelectionToolbarOverflowable oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
// If the children are changing at all, the current page should be reset.
|
|
if (!listEquals(widget.children, oldWidget.children)) {
|
|
_reset();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(debugCheckHasMaterialLocalizations(context));
|
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
|
|
return _TextSelectionToolbarTrailingEdgeAlign(
|
|
key: _containerKey,
|
|
overflowOpen: _overflowOpen,
|
|
textDirection: Directionality.of(context),
|
|
child: AnimatedSize(
|
|
// This duration was eyeballed on a Pixel 2 emulator running Android
|
|
// API 28.
|
|
duration: const Duration(milliseconds: 140),
|
|
child: widget.toolbarBuilder(context, _TextSelectionToolbarItemsLayout(
|
|
isAbove: widget.isAbove,
|
|
overflowOpen: _overflowOpen,
|
|
children: <Widget>[
|
|
// TODO(justinmc): This overflow button should have its own slot in
|
|
// _TextSelectionToolbarItemsLayout separate from children, similar
|
|
// to how it's done in Cupertino's text selection menu.
|
|
// https://github.com/flutter/flutter/issues/69908
|
|
// The navButton that shows and hides the overflow menu is the
|
|
// first child.
|
|
_TextSelectionToolbarOverflowButton(
|
|
icon: Icon(_overflowOpen ? Icons.arrow_back : Icons.more_vert),
|
|
onPressed: () {
|
|
setState(() {
|
|
_overflowOpen = !_overflowOpen;
|
|
});
|
|
},
|
|
tooltip: _overflowOpen
|
|
? localizations.backButtonTooltip
|
|
: localizations.moreButtonTooltip,
|
|
),
|
|
...widget.children,
|
|
],
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// When the overflow menu is open, it tries to align its trailing edge to the
|
|
// trailing edge of the closed menu. This widget handles this effect by
|
|
// measuring and maintaining the width of the closed menu and aligning the child
|
|
// to that side.
|
|
class _TextSelectionToolbarTrailingEdgeAlign extends SingleChildRenderObjectWidget {
|
|
const _TextSelectionToolbarTrailingEdgeAlign({
|
|
super.key,
|
|
required Widget super.child,
|
|
required this.overflowOpen,
|
|
required this.textDirection,
|
|
});
|
|
|
|
final bool overflowOpen;
|
|
final TextDirection textDirection;
|
|
|
|
@override
|
|
_TextSelectionToolbarTrailingEdgeAlignRenderBox createRenderObject(BuildContext context) {
|
|
return _TextSelectionToolbarTrailingEdgeAlignRenderBox(
|
|
overflowOpen: overflowOpen,
|
|
textDirection: textDirection,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _TextSelectionToolbarTrailingEdgeAlignRenderBox renderObject) {
|
|
renderObject
|
|
..overflowOpen = overflowOpen
|
|
..textDirection = textDirection;
|
|
}
|
|
}
|
|
|
|
class _TextSelectionToolbarTrailingEdgeAlignRenderBox extends RenderProxyBox {
|
|
_TextSelectionToolbarTrailingEdgeAlignRenderBox({
|
|
required bool overflowOpen,
|
|
required TextDirection textDirection,
|
|
}) : _textDirection = textDirection,
|
|
_overflowOpen = overflowOpen,
|
|
super();
|
|
|
|
// The width of the menu when it was closed. This is used to achieve the
|
|
// behavior where the open menu aligns its trailing edge to the closed menu's
|
|
// trailing edge.
|
|
double? _closedWidth;
|
|
|
|
bool _overflowOpen;
|
|
bool get overflowOpen => _overflowOpen;
|
|
set overflowOpen(bool value) {
|
|
if (value == overflowOpen) {
|
|
return;
|
|
}
|
|
_overflowOpen = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
TextDirection _textDirection;
|
|
TextDirection get textDirection => _textDirection;
|
|
set textDirection(TextDirection value) {
|
|
if (value == textDirection) {
|
|
return;
|
|
}
|
|
_textDirection = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
child!.layout(constraints.loosen(), parentUsesSize: true);
|
|
|
|
// Save the width when the menu is closed. If the menu changes, this width
|
|
// is invalid, so it's important that this RenderBox be recreated in that
|
|
// case. Currently, this is achieved by providing a new key to
|
|
// _TextSelectionToolbarTrailingEdgeAlign.
|
|
if (!overflowOpen && _closedWidth == null) {
|
|
_closedWidth = child!.size.width;
|
|
}
|
|
|
|
size = constraints.constrain(Size(
|
|
// If the open menu is wider than the closed menu, just use its own width
|
|
// and don't worry about aligning the trailing edges.
|
|
// _closedWidth is used even when the menu is closed to allow it to
|
|
// animate its size while keeping the same edge alignment.
|
|
_closedWidth == null || child!.size.width > _closedWidth! ? child!.size.width : _closedWidth!,
|
|
child!.size.height,
|
|
));
|
|
|
|
// Set the offset in the parent data such that the child will be aligned to
|
|
// the trailing edge, depending on the text direction.
|
|
final ToolbarItemsParentData childParentData = child!.parentData! as ToolbarItemsParentData;
|
|
childParentData.offset = Offset(
|
|
textDirection == TextDirection.rtl ? 0.0 : size.width - child!.size.width,
|
|
0.0,
|
|
);
|
|
}
|
|
|
|
// Paint at the offset set in the parent data.
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
final ToolbarItemsParentData childParentData = child!.parentData! as ToolbarItemsParentData;
|
|
context.paintChild(child!, childParentData.offset + offset);
|
|
}
|
|
|
|
// Include the parent data offset in the hit test.
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
|
// The x, y parameters have the top left of the node's box as the origin.
|
|
final ToolbarItemsParentData childParentData = child!.parentData! as ToolbarItemsParentData;
|
|
return result.addWithPaintOffset(
|
|
offset: childParentData.offset,
|
|
position: position,
|
|
hitTest: (BoxHitTestResult result, Offset transformed) {
|
|
assert(transformed == position - childParentData.offset);
|
|
return child!.hitTest(result, position: transformed);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderBox child) {
|
|
if (child.parentData is! ToolbarItemsParentData) {
|
|
child.parentData = ToolbarItemsParentData();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
|
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
|
|
transform.translate(childParentData.offset.dx, childParentData.offset.dy);
|
|
super.applyPaintTransform(child, transform);
|
|
}
|
|
}
|
|
|
|
// Renders the menu items in the correct positions in the menu and its overflow
|
|
// submenu based on calculating which item would first overflow.
|
|
class _TextSelectionToolbarItemsLayout extends MultiChildRenderObjectWidget {
|
|
const _TextSelectionToolbarItemsLayout({
|
|
required this.isAbove,
|
|
required this.overflowOpen,
|
|
required super.children,
|
|
});
|
|
|
|
final bool isAbove;
|
|
final bool overflowOpen;
|
|
|
|
@override
|
|
_RenderTextSelectionToolbarItemsLayout createRenderObject(BuildContext context) {
|
|
return _RenderTextSelectionToolbarItemsLayout(
|
|
isAbove: isAbove,
|
|
overflowOpen: overflowOpen,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _RenderTextSelectionToolbarItemsLayout renderObject) {
|
|
renderObject
|
|
..isAbove = isAbove
|
|
..overflowOpen = overflowOpen;
|
|
}
|
|
|
|
@override
|
|
_TextSelectionToolbarItemsLayoutElement createElement() => _TextSelectionToolbarItemsLayoutElement(this);
|
|
}
|
|
|
|
class _TextSelectionToolbarItemsLayoutElement extends MultiChildRenderObjectElement {
|
|
_TextSelectionToolbarItemsLayoutElement(
|
|
super.widget,
|
|
);
|
|
|
|
static bool _shouldPaint(Element child) {
|
|
return (child.renderObject!.parentData! as ToolbarItemsParentData).shouldPaint;
|
|
}
|
|
|
|
@override
|
|
void debugVisitOnstageChildren(ElementVisitor visitor) {
|
|
children.where(_shouldPaint).forEach(visitor);
|
|
}
|
|
}
|
|
|
|
class _RenderTextSelectionToolbarItemsLayout extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData> {
|
|
_RenderTextSelectionToolbarItemsLayout({
|
|
required bool isAbove,
|
|
required bool overflowOpen,
|
|
}) : _isAbove = isAbove,
|
|
_overflowOpen = overflowOpen,
|
|
super();
|
|
|
|
// The index of the last item that doesn't overflow.
|
|
int _lastIndexThatFits = -1;
|
|
|
|
bool _isAbove;
|
|
bool get isAbove => _isAbove;
|
|
set isAbove(bool value) {
|
|
if (value == isAbove) {
|
|
return;
|
|
}
|
|
_isAbove = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
bool _overflowOpen;
|
|
bool get overflowOpen => _overflowOpen;
|
|
set overflowOpen(bool value) {
|
|
if (value == overflowOpen) {
|
|
return;
|
|
}
|
|
_overflowOpen = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
// Layout the necessary children, and figure out where the children first
|
|
// overflow, if at all.
|
|
void _layoutChildren() {
|
|
// When overflow is not open, the toolbar is always a specific height.
|
|
final BoxConstraints sizedConstraints = _overflowOpen
|
|
? constraints
|
|
: BoxConstraints.loose(Size(
|
|
constraints.maxWidth,
|
|
_kToolbarHeight,
|
|
));
|
|
|
|
int i = -1;
|
|
double width = 0.0;
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
i++;
|
|
|
|
// No need to layout children inside the overflow menu when it's closed.
|
|
// The opposite is not true. It is necessary to layout the children that
|
|
// don't overflow when the overflow menu is open in order to calculate
|
|
// _lastIndexThatFits.
|
|
if (_lastIndexThatFits != -1 && !overflowOpen) {
|
|
return;
|
|
}
|
|
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
child.layout(sizedConstraints.loosen(), parentUsesSize: true);
|
|
width += child.size.width;
|
|
|
|
if (width > sizedConstraints.maxWidth && _lastIndexThatFits == -1) {
|
|
_lastIndexThatFits = i - 1;
|
|
}
|
|
});
|
|
|
|
// If the last child overflows, but only because of the width of the
|
|
// overflow button, then just show it and hide the overflow button.
|
|
final RenderBox navButton = firstChild!;
|
|
if (_lastIndexThatFits != -1
|
|
&& _lastIndexThatFits == childCount - 2
|
|
&& width - navButton.size.width <= sizedConstraints.maxWidth) {
|
|
_lastIndexThatFits = -1;
|
|
}
|
|
}
|
|
|
|
// Returns true when the child should be painted, false otherwise.
|
|
bool _shouldPaintChild(RenderObject renderObjectChild, int index) {
|
|
// Paint the navButton when there is overflow.
|
|
if (renderObjectChild == firstChild) {
|
|
return _lastIndexThatFits != -1;
|
|
}
|
|
|
|
// If there is no overflow, all children besides the navButton are painted.
|
|
if (_lastIndexThatFits == -1) {
|
|
return true;
|
|
}
|
|
|
|
// When there is overflow, paint if the child is in the part of the menu
|
|
// that is currently open. Overflowing children are painted when the
|
|
// overflow menu is open, and the children that fit are painted when the
|
|
// overflow menu is closed.
|
|
return (index > _lastIndexThatFits) == overflowOpen;
|
|
}
|
|
|
|
// Decide which children will be painted, set their shouldPaint, and set the
|
|
// offset that painted children will be placed at.
|
|
void _placeChildren() {
|
|
int i = -1;
|
|
Size nextSize = Size.zero;
|
|
double fitWidth = 0.0;
|
|
final RenderBox navButton = firstChild!;
|
|
double overflowHeight = overflowOpen && !isAbove ? navButton.size.height : 0.0;
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
i++;
|
|
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
|
|
|
|
// Handle placing the navigation button after iterating all children.
|
|
if (renderObjectChild == navButton) {
|
|
return;
|
|
}
|
|
|
|
// There is no need to place children that won't be painted.
|
|
if (!_shouldPaintChild(renderObjectChild, i)) {
|
|
childParentData.shouldPaint = false;
|
|
return;
|
|
}
|
|
childParentData.shouldPaint = true;
|
|
|
|
if (!overflowOpen) {
|
|
childParentData.offset = Offset(fitWidth, 0.0);
|
|
fitWidth += child.size.width;
|
|
nextSize = Size(
|
|
fitWidth,
|
|
math.max(child.size.height, nextSize.height),
|
|
);
|
|
} else {
|
|
childParentData.offset = Offset(0.0, overflowHeight);
|
|
overflowHeight += child.size.height;
|
|
nextSize = Size(
|
|
math.max(child.size.width, nextSize.width),
|
|
overflowHeight,
|
|
);
|
|
}
|
|
});
|
|
|
|
// Place the navigation button if needed.
|
|
final ToolbarItemsParentData navButtonParentData = navButton.parentData! as ToolbarItemsParentData;
|
|
if (_shouldPaintChild(firstChild!, 0)) {
|
|
navButtonParentData.shouldPaint = true;
|
|
if (overflowOpen) {
|
|
navButtonParentData.offset = isAbove
|
|
? Offset(0.0, overflowHeight)
|
|
: Offset.zero;
|
|
nextSize = Size(
|
|
nextSize.width,
|
|
isAbove ? nextSize.height + navButton.size.height : nextSize.height,
|
|
);
|
|
} else {
|
|
navButtonParentData.offset = Offset(fitWidth, 0.0);
|
|
nextSize = Size(nextSize.width + navButton.size.width, nextSize.height);
|
|
}
|
|
} else {
|
|
navButtonParentData.shouldPaint = false;
|
|
}
|
|
|
|
size = nextSize;
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
_lastIndexThatFits = -1;
|
|
if (firstChild == null) {
|
|
size = constraints.smallest;
|
|
return;
|
|
}
|
|
|
|
_layoutChildren();
|
|
_placeChildren();
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
|
|
if (!childParentData.shouldPaint) {
|
|
return;
|
|
}
|
|
|
|
context.paintChild(child, childParentData.offset + offset);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderBox child) {
|
|
if (child.parentData is! ToolbarItemsParentData) {
|
|
child.parentData = ToolbarItemsParentData();
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
|
RenderBox? child = lastChild;
|
|
while (child != null) {
|
|
// The x, y parameters have the top left of the node's box as the origin.
|
|
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
|
|
|
|
// Don't hit test children aren't shown.
|
|
if (!childParentData.shouldPaint) {
|
|
child = childParentData.previousSibling;
|
|
continue;
|
|
}
|
|
|
|
final bool isHit = result.addWithPaintOffset(
|
|
offset: childParentData.offset,
|
|
position: position,
|
|
hitTest: (BoxHitTestResult result, Offset transformed) {
|
|
assert(transformed == position - childParentData.offset);
|
|
return child!.hitTest(result, position: transformed);
|
|
},
|
|
);
|
|
if (isHit) {
|
|
return true;
|
|
}
|
|
child = childParentData.previousSibling;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Visit only the children that should be painted.
|
|
@override
|
|
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
|
|
if (childParentData.shouldPaint) {
|
|
visitor(renderObjectChild);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// The Material-styled toolbar outline. Fill it with any widgets you want. No
|
|
// overflow ability.
|
|
class _TextSelectionToolbarContainer extends StatelessWidget {
|
|
const _TextSelectionToolbarContainer({
|
|
required this.child,
|
|
});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
// This value was eyeballed to match the native text selection menu on
|
|
// a Pixel 2 running Android 10.
|
|
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
|
clipBehavior: Clip.antiAlias,
|
|
elevation: 1.0,
|
|
type: MaterialType.card,
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
// A button styled like a Material native Android text selection overflow menu
|
|
// forward and back controls.
|
|
class _TextSelectionToolbarOverflowButton extends StatelessWidget {
|
|
const _TextSelectionToolbarOverflowButton({
|
|
required this.icon,
|
|
this.onPressed,
|
|
this.tooltip,
|
|
});
|
|
|
|
final Icon icon;
|
|
final VoidCallback? onPressed;
|
|
final String? tooltip;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
type: MaterialType.card,
|
|
color: const Color(0x00000000),
|
|
child: IconButton(
|
|
// TODO(justinmc): This should be an AnimatedIcon, but
|
|
// AnimatedIcons doesn't yet support arrow_back to more_vert.
|
|
// https://github.com/flutter/flutter/issues/51209
|
|
icon: icon,
|
|
onPressed: onPressed,
|
|
tooltip: tooltip,
|
|
),
|
|
);
|
|
}
|
|
}
|