
Adds MediaQuery.maybeOf to replace calling MediaQuery.of(context, nullOk: true), and removes the nullOk parameter. Also changes MediaQuery.of to return a non-nullable value, and removes many instances of the ! operator, reducing the possible places where a null dereference could occur.
1259 lines
42 KiB
Dart
1259 lines
42 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:collection';
|
|
import 'dart:math' as math;
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'button.dart';
|
|
import 'colors.dart';
|
|
import 'localizations.dart';
|
|
import 'theme.dart';
|
|
|
|
// Read off from the output on iOS 12. This color does not vary with the
|
|
// application's theme color.
|
|
const double _kSelectionHandleOverlap = 1.5;
|
|
// Extracted from https://developer.apple.com/design/resources/.
|
|
const double _kSelectionHandleRadius = 6;
|
|
|
|
// Minimal padding from all edges of the selection toolbar to all edges of the
|
|
// screen.
|
|
const double _kToolbarScreenPadding = 8.0;
|
|
// Minimal padding from tip of the selection toolbar arrow to horizontal edges of the
|
|
// screen. Eyeballed value.
|
|
const double _kArrowScreenPadding = 26.0;
|
|
|
|
// Vertical distance between the tip of the arrow and the line of text the arrow
|
|
// is pointing to. The value used here is eyeballed.
|
|
const double _kToolbarContentDistance = 8.0;
|
|
// Values derived from https://developer.apple.com/design/resources/.
|
|
// 92% Opacity ~= 0xEB
|
|
|
|
// Values extracted from https://developer.apple.com/design/resources/.
|
|
// The height of the toolbar, including the arrow.
|
|
const double _kToolbarHeight = 43.0;
|
|
const Size _kToolbarArrowSize = Size(14.0, 7.0);
|
|
const Radius _kToolbarBorderRadius = Radius.circular(8);
|
|
// Colors extracted from https://developer.apple.com/design/resources/.
|
|
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
|
|
const Color _kToolbarBackgroundColor = Color(0xEB202020);
|
|
const Color _kToolbarDividerColor = Color(0xFF808080);
|
|
|
|
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
|
inherit: false,
|
|
fontSize: 14.0,
|
|
letterSpacing: -0.15,
|
|
fontWeight: FontWeight.w400,
|
|
color: CupertinoColors.white,
|
|
);
|
|
|
|
const TextStyle _kToolbarButtonDisabledFontStyle = TextStyle(
|
|
inherit: false,
|
|
fontSize: 14.0,
|
|
letterSpacing: -0.15,
|
|
fontWeight: FontWeight.w400,
|
|
color: CupertinoColors.inactiveGray,
|
|
);
|
|
|
|
// Eyeballed value.
|
|
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0);
|
|
|
|
// Generates the child that's passed into CupertinoTextSelectionToolbar.
|
|
class _CupertinoTextSelectionToolbarWrapper extends StatefulWidget {
|
|
const _CupertinoTextSelectionToolbarWrapper({
|
|
Key? key,
|
|
required this.arrowTipX,
|
|
required this.barTopY,
|
|
this.clipboardStatus,
|
|
this.handleCut,
|
|
this.handleCopy,
|
|
this.handlePaste,
|
|
this.handleSelectAll,
|
|
required this.isArrowPointingDown,
|
|
}) : super(key: key);
|
|
|
|
final double arrowTipX;
|
|
final double barTopY;
|
|
final ClipboardStatusNotifier? clipboardStatus;
|
|
final VoidCallback? handleCut;
|
|
final VoidCallback? handleCopy;
|
|
final VoidCallback? handlePaste;
|
|
final VoidCallback? handleSelectAll;
|
|
final bool isArrowPointingDown;
|
|
|
|
@override
|
|
_CupertinoTextSelectionToolbarWrapperState createState() => _CupertinoTextSelectionToolbarWrapperState();
|
|
}
|
|
|
|
class _CupertinoTextSelectionToolbarWrapperState extends State<_CupertinoTextSelectionToolbarWrapper> {
|
|
late ClipboardStatusNotifier _clipboardStatus;
|
|
|
|
void _onChangedClipboardStatus() {
|
|
setState(() {
|
|
// Inform the widget that the value of clipboardStatus has changed.
|
|
});
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier();
|
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
|
_clipboardStatus.update();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_CupertinoTextSelectionToolbarWrapper oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.clipboardStatus == null && widget.clipboardStatus != null) {
|
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
|
_clipboardStatus.dispose();
|
|
_clipboardStatus = widget.clipboardStatus!;
|
|
} else if (oldWidget.clipboardStatus != null) {
|
|
if (widget.clipboardStatus == null) {
|
|
_clipboardStatus = ClipboardStatusNotifier();
|
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
|
oldWidget.clipboardStatus!.removeListener(_onChangedClipboardStatus);
|
|
} else if (widget.clipboardStatus != oldWidget.clipboardStatus) {
|
|
_clipboardStatus = widget.clipboardStatus!;
|
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
|
oldWidget.clipboardStatus!.removeListener(_onChangedClipboardStatus);
|
|
}
|
|
}
|
|
if (widget.handlePaste != null) {
|
|
_clipboardStatus.update();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
// When used in an Overlay, this can be disposed after its creator has
|
|
// already disposed _clipboardStatus.
|
|
if (!_clipboardStatus.disposed) {
|
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
|
if (widget.clipboardStatus == null) {
|
|
_clipboardStatus.dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Don't render the menu until the state of the clipboard is known.
|
|
if (widget.handlePaste != null
|
|
&& _clipboardStatus.value == ClipboardStatus.unknown) {
|
|
return const SizedBox(width: 0.0, height: 0.0);
|
|
}
|
|
|
|
final List<Widget> items = <Widget>[];
|
|
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
|
final EdgeInsets arrowPadding = widget.isArrowPointingDown
|
|
? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
|
|
: EdgeInsets.only(top: _kToolbarArrowSize.height);
|
|
final Widget onePhysicalPixelVerticalDivider =
|
|
SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
|
|
|
void addToolbarButton(
|
|
String text,
|
|
VoidCallback onPressed,
|
|
) {
|
|
if (items.isNotEmpty) {
|
|
items.add(onePhysicalPixelVerticalDivider);
|
|
}
|
|
|
|
items.add(CupertinoButton(
|
|
child: Text(
|
|
text,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: _kToolbarButtonFontStyle,
|
|
),
|
|
borderRadius: null,
|
|
color: _kToolbarBackgroundColor,
|
|
minSize: _kToolbarHeight,
|
|
onPressed: onPressed,
|
|
padding: _kToolbarButtonPadding.add(arrowPadding),
|
|
pressedOpacity: 0.7,
|
|
));
|
|
}
|
|
|
|
if (widget.handleCut != null) {
|
|
addToolbarButton(localizations.cutButtonLabel, widget.handleCut!);
|
|
}
|
|
if (widget.handleCopy != null) {
|
|
addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!);
|
|
}
|
|
if (widget.handlePaste != null
|
|
&& _clipboardStatus.value == ClipboardStatus.pasteable) {
|
|
addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!);
|
|
}
|
|
if (widget.handleSelectAll != null) {
|
|
addToolbarButton(localizations.selectAllButtonLabel, widget.handleSelectAll!);
|
|
}
|
|
|
|
return CupertinoTextSelectionToolbar._(
|
|
barTopY: widget.barTopY,
|
|
arrowTipX: widget.arrowTipX,
|
|
isArrowPointingDown: widget.isArrowPointingDown,
|
|
child: items.isEmpty ? null : _CupertinoTextSelectionToolbarContent(
|
|
isArrowPointingDown: widget.isArrowPointingDown,
|
|
children: items,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// An iOS-style toolbar that appears in response to text selection.
|
|
///
|
|
/// Typically displays buttons for text manipulation, e.g. copying and pasting text.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [TextSelectionControls.buildToolbar], where [CupertinoTextSelectionToolbar]
|
|
/// will be used to build an iOS-style toolbar.
|
|
@visibleForTesting
|
|
class CupertinoTextSelectionToolbar extends SingleChildRenderObjectWidget {
|
|
const CupertinoTextSelectionToolbar._({
|
|
Key? key,
|
|
required double barTopY,
|
|
required double arrowTipX,
|
|
required bool isArrowPointingDown,
|
|
Widget? child,
|
|
}) : _barTopY = barTopY,
|
|
_arrowTipX = arrowTipX,
|
|
_isArrowPointingDown = isArrowPointingDown,
|
|
super(key: key, child: child);
|
|
|
|
// The y-coordinate of toolbar's top edge, in global coordinate system.
|
|
final double _barTopY;
|
|
|
|
// The y-coordinate of the tip of the arrow, in global coordinate system.
|
|
final double _arrowTipX;
|
|
|
|
// Whether the arrow should point down and be attached to the bottom
|
|
// of the toolbar, or point up and be attached to the top of the toolbar.
|
|
final bool _isArrowPointingDown;
|
|
|
|
@override
|
|
_ToolbarRenderBox createRenderObject(BuildContext context) => _ToolbarRenderBox(_barTopY, _arrowTipX, _isArrowPointingDown, null);
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _ToolbarRenderBox renderObject) {
|
|
renderObject
|
|
..barTopY = _barTopY
|
|
..arrowTipX = _arrowTipX
|
|
..isArrowPointingDown = _isArrowPointingDown;
|
|
}
|
|
}
|
|
|
|
class _ToolbarParentData extends BoxParentData {
|
|
// The x offset from the tip of the arrow to the center of the toolbar.
|
|
// Positive if the tip of the arrow has a larger x-coordinate than the
|
|
// center of the toolbar.
|
|
double? arrowXOffsetFromCenter;
|
|
@override
|
|
String toString() => 'offset=$offset, arrowXOffsetFromCenter=$arrowXOffsetFromCenter';
|
|
}
|
|
|
|
class _ToolbarRenderBox extends RenderShiftedBox {
|
|
_ToolbarRenderBox(
|
|
this._barTopY,
|
|
this._arrowTipX,
|
|
this._isArrowPointingDown,
|
|
RenderBox? child,
|
|
) : super(child);
|
|
|
|
|
|
@override
|
|
bool get isRepaintBoundary => true;
|
|
|
|
double _barTopY;
|
|
set barTopY(double value) {
|
|
if (_barTopY == value) {
|
|
return;
|
|
}
|
|
_barTopY = value;
|
|
markNeedsLayout();
|
|
markNeedsSemanticsUpdate();
|
|
}
|
|
|
|
double _arrowTipX;
|
|
set arrowTipX(double value) {
|
|
if (_arrowTipX == value) {
|
|
return;
|
|
}
|
|
_arrowTipX = value;
|
|
markNeedsLayout();
|
|
markNeedsSemanticsUpdate();
|
|
}
|
|
|
|
bool _isArrowPointingDown;
|
|
set isArrowPointingDown(bool value) {
|
|
if (_isArrowPointingDown == value) {
|
|
return;
|
|
}
|
|
_isArrowPointingDown = value;
|
|
markNeedsLayout();
|
|
markNeedsSemanticsUpdate();
|
|
}
|
|
|
|
final BoxConstraints heightConstraint = const BoxConstraints.tightFor(height: _kToolbarHeight);
|
|
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! _ToolbarParentData) {
|
|
child.parentData = _ToolbarParentData();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
final BoxConstraints constraints = this.constraints;
|
|
size = constraints.biggest;
|
|
|
|
if (child == null) {
|
|
return;
|
|
}
|
|
final BoxConstraints enforcedConstraint = constraints
|
|
.deflate(const EdgeInsets.symmetric(horizontal: _kToolbarScreenPadding))
|
|
.loosen();
|
|
|
|
child!.layout(heightConstraint.enforce(enforcedConstraint), parentUsesSize: true,);
|
|
final _ToolbarParentData childParentData = child!.parentData! as _ToolbarParentData;
|
|
|
|
// The local x-coordinate of the center of the toolbar.
|
|
final double lowerBound = child!.size.width/2 + _kToolbarScreenPadding;
|
|
final double upperBound = size.width - child!.size.width/2 - _kToolbarScreenPadding;
|
|
final double adjustedCenterX = _arrowTipX.clamp(lowerBound, upperBound);
|
|
|
|
childParentData.offset = Offset(adjustedCenterX - child!.size.width / 2, _barTopY);
|
|
childParentData.arrowXOffsetFromCenter = _arrowTipX - adjustedCenterX;
|
|
}
|
|
|
|
// The path is described in the toolbar's coordinate system.
|
|
Path _clipPath() {
|
|
final _ToolbarParentData childParentData = child!.parentData! as _ToolbarParentData;
|
|
final Path rrect = Path()
|
|
..addRRect(
|
|
RRect.fromRectAndRadius(
|
|
Offset(0, _isArrowPointingDown ? 0 : _kToolbarArrowSize.height)
|
|
& Size(child!.size.width, child!.size.height - _kToolbarArrowSize.height),
|
|
_kToolbarBorderRadius,
|
|
),
|
|
);
|
|
|
|
final double arrowTipX = child!.size.width / 2 + childParentData.arrowXOffsetFromCenter!;
|
|
|
|
final double arrowBottomY = _isArrowPointingDown
|
|
? child!.size.height - _kToolbarArrowSize.height
|
|
: _kToolbarArrowSize.height;
|
|
|
|
final double arrowTipY = _isArrowPointingDown ? child!.size.height : 0;
|
|
|
|
final Path arrow = Path()
|
|
..moveTo(arrowTipX, arrowTipY)
|
|
..lineTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBottomY)
|
|
..lineTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBottomY)
|
|
..close();
|
|
|
|
return Path.combine(PathOperation.union, rrect, arrow);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (child == null) {
|
|
return;
|
|
}
|
|
|
|
final _ToolbarParentData childParentData = child!.parentData! as _ToolbarParentData;
|
|
_clipPathLayer = context.pushClipPath(
|
|
needsCompositing,
|
|
offset + childParentData.offset,
|
|
Offset.zero & child!.size,
|
|
_clipPath(),
|
|
(PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child!, innerOffset),
|
|
oldLayer: _clipPathLayer
|
|
);
|
|
}
|
|
|
|
ClipPathLayer? _clipPathLayer;
|
|
Paint? _debugPaint;
|
|
|
|
@override
|
|
void debugPaintSize(PaintingContext context, Offset offset) {
|
|
assert(() {
|
|
if (child == null) {
|
|
return true;
|
|
}
|
|
|
|
_debugPaint ??= Paint()
|
|
..shader = ui.Gradient.linear(
|
|
const Offset(0.0, 0.0),
|
|
const Offset(10.0, 10.0),
|
|
const <Color>[Color(0x00000000), Color(0xFFFF00FF), Color(0xFFFF00FF), Color(0x00000000)],
|
|
const <double>[0.25, 0.25, 0.75, 0.75],
|
|
TileMode.repeated,
|
|
)
|
|
..strokeWidth = 2.0
|
|
..style = PaintingStyle.stroke;
|
|
|
|
final _ToolbarParentData childParentData = child!.parentData! as _ToolbarParentData;
|
|
context.canvas.drawPath(_clipPath().shift(offset + childParentData.offset), _debugPaint!);
|
|
return true;
|
|
}());
|
|
}
|
|
}
|
|
|
|
/// Draws a single text selection handle with a bar and a ball.
|
|
class _TextSelectionHandlePainter extends CustomPainter {
|
|
const _TextSelectionHandlePainter(this.color);
|
|
|
|
final Color color;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
const double halfStrokeWidth = 1.0;
|
|
final Paint paint = Paint()..color = color;
|
|
final Rect circle = Rect.fromCircle(
|
|
center: const Offset(_kSelectionHandleRadius, _kSelectionHandleRadius),
|
|
radius: _kSelectionHandleRadius,
|
|
);
|
|
final Rect line = Rect.fromPoints(
|
|
const Offset(
|
|
_kSelectionHandleRadius - halfStrokeWidth,
|
|
2 * _kSelectionHandleRadius - _kSelectionHandleOverlap,
|
|
),
|
|
Offset(_kSelectionHandleRadius + halfStrokeWidth, size.height),
|
|
);
|
|
final Path path = Path()
|
|
..addOval(circle)
|
|
// Draw line so it slightly overlaps the circle.
|
|
..addRect(line);
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => color != oldPainter.color;
|
|
}
|
|
|
|
class _CupertinoTextSelectionControls extends TextSelectionControls {
|
|
/// Returns the size of the Cupertino handle.
|
|
@override
|
|
Size getHandleSize(double textLineHeight) {
|
|
return Size(
|
|
_kSelectionHandleRadius * 2,
|
|
textLineHeight + _kSelectionHandleRadius * 2 - _kSelectionHandleOverlap,
|
|
);
|
|
}
|
|
|
|
/// Builder for iOS-style copy/paste text selection toolbar.
|
|
@override
|
|
Widget buildToolbar(
|
|
BuildContext context,
|
|
Rect globalEditableRegion,
|
|
double textLineHeight,
|
|
Offset position,
|
|
List<TextSelectionPoint> endpoints,
|
|
TextSelectionDelegate delegate,
|
|
ClipboardStatusNotifier clipboardStatus,
|
|
) {
|
|
assert(debugCheckHasMediaQuery(context));
|
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
|
|
|
// The toolbar should appear below the TextField when there is not enough
|
|
// space above the TextField to show it, assuming there's always enough space
|
|
// at the bottom in this case.
|
|
final double toolbarHeightNeeded = mediaQuery.padding.top
|
|
+ _kToolbarScreenPadding
|
|
+ _kToolbarHeight
|
|
+ _kToolbarContentDistance;
|
|
final double availableHeight = globalEditableRegion.top + endpoints.first.point.dy - textLineHeight;
|
|
final bool isArrowPointingDown = toolbarHeightNeeded <= availableHeight;
|
|
|
|
final double arrowTipX = (position.dx + globalEditableRegion.left).clamp(
|
|
_kArrowScreenPadding + mediaQuery.padding.left,
|
|
mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
|
|
);
|
|
|
|
// The y-coordinate has to be calculated instead of directly quoting position.dy,
|
|
// since the caller (TextSelectionOverlay._buildToolbar) does not know whether
|
|
// the toolbar is going to be facing up or down.
|
|
final double localBarTopY = isArrowPointingDown
|
|
? endpoints.first.point.dy - textLineHeight - _kToolbarContentDistance - _kToolbarHeight
|
|
: endpoints.last.point.dy + _kToolbarContentDistance;
|
|
|
|
return _CupertinoTextSelectionToolbarWrapper(
|
|
arrowTipX: arrowTipX,
|
|
barTopY: localBarTopY + globalEditableRegion.top,
|
|
clipboardStatus: clipboardStatus,
|
|
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
|
|
handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null,
|
|
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
|
|
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
|
|
isArrowPointingDown: isArrowPointingDown,
|
|
);
|
|
}
|
|
|
|
/// Builder for iOS text selection edges.
|
|
@override
|
|
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
|
|
// We want a size that's a vertical line the height of the text plus a 18.0
|
|
// padding in every direction that will constitute the selection drag area.
|
|
final Size desiredSize = getHandleSize(textLineHeight);
|
|
|
|
final Widget handle = SizedBox.fromSize(
|
|
size: desiredSize,
|
|
child: CustomPaint(
|
|
painter: _TextSelectionHandlePainter(CupertinoTheme.of(context).primaryColor),
|
|
),
|
|
);
|
|
|
|
// [buildHandle]'s widget is positioned at the selection cursor's bottom
|
|
// baseline. We transform the handle such that the SizedBox is superimposed
|
|
// on top of the text selection endpoints.
|
|
switch (type) {
|
|
case TextSelectionHandleType.left:
|
|
return handle;
|
|
case TextSelectionHandleType.right:
|
|
// Right handle is a vertical mirror of the left.
|
|
return Transform(
|
|
transform: Matrix4.identity()
|
|
..translate(desiredSize.width / 2, desiredSize.height / 2)
|
|
..rotateZ(math.pi)
|
|
..translate(-desiredSize.width / 2, -desiredSize.height / 2),
|
|
child: handle,
|
|
);
|
|
// iOS doesn't draw anything for collapsed selections.
|
|
case TextSelectionHandleType.collapsed:
|
|
return const SizedBox();
|
|
}
|
|
}
|
|
|
|
/// Gets anchor for cupertino-style text selection handles.
|
|
///
|
|
/// See [TextSelectionControls.getHandleAnchor].
|
|
@override
|
|
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
|
|
final Size handleSize = getHandleSize(textLineHeight);
|
|
switch (type) {
|
|
// The circle is at the top for the left handle, and the anchor point is
|
|
// all the way at the bottom of the line.
|
|
case TextSelectionHandleType.left:
|
|
return Offset(
|
|
handleSize.width / 2,
|
|
handleSize.height,
|
|
);
|
|
// The right handle is vertically flipped, and the anchor point is near
|
|
// the top of the circle to give slight overlap.
|
|
case TextSelectionHandleType.right:
|
|
return Offset(
|
|
handleSize.width / 2,
|
|
handleSize.height - 2 * _kSelectionHandleRadius + _kSelectionHandleOverlap,
|
|
);
|
|
// A collapsed handle anchors itself so that it's centered.
|
|
case TextSelectionHandleType.collapsed:
|
|
return Offset(
|
|
handleSize.width / 2,
|
|
textLineHeight + (handleSize.height - textLineHeight) / 2,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Renders the content of the selection menu and maintains the page state.
|
|
class _CupertinoTextSelectionToolbarContent extends StatefulWidget {
|
|
const _CupertinoTextSelectionToolbarContent({
|
|
Key? key,
|
|
required this.children,
|
|
required this.isArrowPointingDown,
|
|
}) : assert(children != null),
|
|
// This ignore is used because .isNotEmpty isn't compatible with const.
|
|
assert(children.length > 0), // ignore: prefer_is_empty
|
|
super(key: key);
|
|
|
|
final List<Widget> children;
|
|
final bool isArrowPointingDown;
|
|
|
|
@override
|
|
_CupertinoTextSelectionToolbarContentState createState() => _CupertinoTextSelectionToolbarContentState();
|
|
}
|
|
|
|
class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSelectionToolbarContent> with TickerProviderStateMixin {
|
|
// Controls the fading of the buttons within the menu during page transitions.
|
|
late AnimationController _controller;
|
|
int _page = 0;
|
|
int? _nextPage;
|
|
|
|
void _handleNextPage() {
|
|
_controller.reverse();
|
|
_controller.addStatusListener(_statusListener);
|
|
_nextPage = _page + 1;
|
|
}
|
|
|
|
void _handlePreviousPage() {
|
|
_controller.reverse();
|
|
_controller.addStatusListener(_statusListener);
|
|
_nextPage = _page - 1;
|
|
}
|
|
|
|
void _statusListener(AnimationStatus status) {
|
|
if (status != AnimationStatus.dismissed) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_page = _nextPage!;
|
|
_nextPage = null;
|
|
});
|
|
_controller.forward();
|
|
_controller.removeStatusListener(_statusListener);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
value: 1.0,
|
|
vsync: this,
|
|
// This was eyeballed on a physical iOS device running iOS 13.
|
|
duration: const Duration(milliseconds: 150),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_CupertinoTextSelectionToolbarContent oldWidget) {
|
|
// If the children are changing, the current page should be reset.
|
|
if (widget.children != oldWidget.children) {
|
|
_page = 0;
|
|
_nextPage = null;
|
|
_controller.forward();
|
|
_controller.removeStatusListener(_statusListener);
|
|
}
|
|
super.didUpdateWidget(oldWidget);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final EdgeInsets arrowPadding = widget.isArrowPointingDown
|
|
? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
|
|
: EdgeInsets.only(top: _kToolbarArrowSize.height);
|
|
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(color: _kToolbarDividerColor),
|
|
child: FadeTransition(
|
|
opacity: _controller,
|
|
child: _CupertinoTextSelectionToolbarItems(
|
|
page: _page,
|
|
backButton: CupertinoButton(
|
|
borderRadius: null,
|
|
color: _kToolbarBackgroundColor,
|
|
minSize: _kToolbarHeight,
|
|
onPressed: _handlePreviousPage,
|
|
padding: arrowPadding,
|
|
pressedOpacity: 0.7,
|
|
child: const Text('◀', style: _kToolbarButtonFontStyle),
|
|
),
|
|
dividerWidth: 1.0 / MediaQuery.of(context).devicePixelRatio,
|
|
nextButton: CupertinoButton(
|
|
borderRadius: null,
|
|
color: _kToolbarBackgroundColor,
|
|
minSize: _kToolbarHeight,
|
|
onPressed: _handleNextPage,
|
|
padding: arrowPadding,
|
|
pressedOpacity: 0.7,
|
|
child: const Text('▶', style: _kToolbarButtonFontStyle),
|
|
),
|
|
nextButtonDisabled: CupertinoButton(
|
|
borderRadius: null,
|
|
color: _kToolbarBackgroundColor,
|
|
disabledColor: _kToolbarBackgroundColor,
|
|
minSize: _kToolbarHeight,
|
|
onPressed: null,
|
|
padding: arrowPadding,
|
|
pressedOpacity: 1.0,
|
|
child: const Text('▶', style: _kToolbarButtonDisabledFontStyle),
|
|
),
|
|
children: widget.children,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// The custom RenderObjectWidget that, together with
|
|
// _CupertinoTextSelectionToolbarItemsRenderBox and
|
|
// _CupertinoTextSelectionToolbarItemsElement, paginates the menu items.
|
|
class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget {
|
|
_CupertinoTextSelectionToolbarItems({
|
|
Key? key,
|
|
required this.page,
|
|
required this.children,
|
|
required this.backButton,
|
|
required this.dividerWidth,
|
|
required this.nextButton,
|
|
required this.nextButtonDisabled,
|
|
}) : assert(children != null),
|
|
assert(children.isNotEmpty),
|
|
assert(backButton != null),
|
|
assert(dividerWidth != null),
|
|
assert(nextButton != null),
|
|
assert(nextButtonDisabled != null),
|
|
assert(page != null),
|
|
super(key: key);
|
|
|
|
final Widget backButton;
|
|
final List<Widget> children;
|
|
final double dividerWidth;
|
|
final Widget nextButton;
|
|
final Widget nextButtonDisabled;
|
|
final int page;
|
|
|
|
@override
|
|
_CupertinoTextSelectionToolbarItemsRenderBox createRenderObject(BuildContext context) {
|
|
return _CupertinoTextSelectionToolbarItemsRenderBox(
|
|
dividerWidth: dividerWidth,
|
|
page: page,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _CupertinoTextSelectionToolbarItemsRenderBox renderObject) {
|
|
renderObject
|
|
..page = page
|
|
..dividerWidth = dividerWidth;
|
|
}
|
|
|
|
@override
|
|
_CupertinoTextSelectionToolbarItemsElement createElement() => _CupertinoTextSelectionToolbarItemsElement(this);
|
|
}
|
|
|
|
// The custom RenderObjectElement that helps paginate the menu items.
|
|
class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
|
|
_CupertinoTextSelectionToolbarItemsElement(
|
|
_CupertinoTextSelectionToolbarItems widget,
|
|
) : super(widget);
|
|
|
|
late List<Element> _children;
|
|
final Map<_CupertinoTextSelectionToolbarItemsSlot, Element> slotToChild = <_CupertinoTextSelectionToolbarItemsSlot, Element>{};
|
|
|
|
// We keep a set of forgotten children to avoid O(n^2) work walking _children
|
|
// repeatedly to remove children.
|
|
final Set<Element> _forgottenChildren = HashSet<Element>();
|
|
|
|
@override
|
|
_CupertinoTextSelectionToolbarItems get widget => super.widget as _CupertinoTextSelectionToolbarItems;
|
|
|
|
@override
|
|
_CupertinoTextSelectionToolbarItemsRenderBox get renderObject => super.renderObject as _CupertinoTextSelectionToolbarItemsRenderBox;
|
|
|
|
void _updateRenderObject(RenderBox? child, _CupertinoTextSelectionToolbarItemsSlot slot) {
|
|
switch (slot) {
|
|
case _CupertinoTextSelectionToolbarItemsSlot.backButton:
|
|
renderObject.backButton = child;
|
|
break;
|
|
case _CupertinoTextSelectionToolbarItemsSlot.nextButton:
|
|
renderObject.nextButton = child;
|
|
break;
|
|
case _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled:
|
|
renderObject.nextButtonDisabled = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void insertRenderObjectChild(RenderObject child, dynamic slot) {
|
|
if (slot is _CupertinoTextSelectionToolbarItemsSlot) {
|
|
assert(child is RenderBox);
|
|
_updateRenderObject(child as RenderBox, slot);
|
|
assert(renderObject.slottedChildren.containsKey(slot));
|
|
return;
|
|
}
|
|
if (slot is IndexedSlot) {
|
|
assert(renderObject.debugValidateChild(child));
|
|
renderObject.insert(child as RenderBox, after: slot.value?.renderObject as RenderBox?);
|
|
return;
|
|
}
|
|
assert(false, 'slot must be _CupertinoTextSelectionToolbarItemsSlot or IndexedSlot');
|
|
}
|
|
|
|
// This is not reachable for children that don't have an IndexedSlot.
|
|
@override
|
|
void moveRenderObjectChild(RenderObject child, IndexedSlot<Element> oldSlot, IndexedSlot<Element> newSlot) {
|
|
assert(child.parent == renderObject);
|
|
renderObject.move(child as RenderBox, after: newSlot.value.renderObject as RenderBox?);
|
|
}
|
|
|
|
static bool _shouldPaint(Element child) {
|
|
return (child.renderObject!.parentData! as ToolbarItemsParentData).shouldPaint;
|
|
}
|
|
|
|
@override
|
|
void removeRenderObjectChild(RenderObject child, dynamic slot) {
|
|
// Check if the child is in a slot.
|
|
if (slot is _CupertinoTextSelectionToolbarItemsSlot) {
|
|
assert(child is RenderBox);
|
|
assert(renderObject.slottedChildren.containsKey(slot));
|
|
_updateRenderObject(null, slot);
|
|
assert(!renderObject.slottedChildren.containsKey(slot));
|
|
return;
|
|
}
|
|
|
|
// Otherwise look for it in the list of children.
|
|
assert(slot is IndexedSlot);
|
|
assert(child.parent == renderObject);
|
|
renderObject.remove(child as RenderBox);
|
|
}
|
|
|
|
@override
|
|
void visitChildren(ElementVisitor visitor) {
|
|
slotToChild.values.forEach(visitor);
|
|
for (final Element child in _children) {
|
|
if (!_forgottenChildren.contains(child))
|
|
visitor(child);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void forgetChild(Element child) {
|
|
assert(slotToChild.containsValue(child) || _children.contains(child));
|
|
assert(!_forgottenChildren.contains(child));
|
|
// Handle forgetting a child in children or in a slot.
|
|
if (slotToChild.containsKey(child.slot)) {
|
|
final _CupertinoTextSelectionToolbarItemsSlot slot = child.slot as _CupertinoTextSelectionToolbarItemsSlot;
|
|
slotToChild.remove(slot);
|
|
} else {
|
|
_forgottenChildren.add(child);
|
|
}
|
|
super.forgetChild(child);
|
|
}
|
|
|
|
// Mount or update slotted child.
|
|
void _mountChild(Widget widget, _CupertinoTextSelectionToolbarItemsSlot slot) {
|
|
final Element? oldChild = slotToChild[slot];
|
|
final Element? newChild = updateChild(oldChild, widget, slot);
|
|
if (oldChild != null) {
|
|
slotToChild.remove(slot);
|
|
}
|
|
if (newChild != null) {
|
|
slotToChild[slot] = newChild;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void mount(Element? parent, dynamic newSlot) {
|
|
super.mount(parent, newSlot);
|
|
// Mount slotted children.
|
|
_mountChild(widget.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
|
|
_mountChild(widget.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
|
|
_mountChild(widget.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
|
|
|
|
// Mount list children.
|
|
_children = List<Element>.filled(widget.children.length, _NullElement.instance);
|
|
Element? previousChild;
|
|
for (int i = 0; i < _children.length; i += 1) {
|
|
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
|
|
_children[i] = newChild;
|
|
previousChild = newChild;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void debugVisitOnstageChildren(ElementVisitor visitor) {
|
|
// Visit slot children.
|
|
for (final Element child in slotToChild.values) {
|
|
if (_shouldPaint(child) && !_forgottenChildren.contains(child)) {
|
|
visitor(child);
|
|
}
|
|
}
|
|
// Visit list children.
|
|
_children
|
|
.where((Element child) => !_forgottenChildren.contains(child) && _shouldPaint(child))
|
|
.forEach(visitor);
|
|
}
|
|
|
|
@override
|
|
void update(_CupertinoTextSelectionToolbarItems newWidget) {
|
|
super.update(newWidget);
|
|
assert(widget == newWidget);
|
|
|
|
// Update slotted children.
|
|
_mountChild(widget.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
|
|
_mountChild(widget.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
|
|
_mountChild(widget.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
|
|
|
|
// Update list children.
|
|
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
|
|
_forgottenChildren.clear();
|
|
}
|
|
}
|
|
|
|
// The custom RenderBox that helps paginate the menu items.
|
|
class _CupertinoTextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData>, RenderBoxContainerDefaultsMixin<RenderBox, ToolbarItemsParentData> {
|
|
_CupertinoTextSelectionToolbarItemsRenderBox({
|
|
required double dividerWidth,
|
|
required int page,
|
|
}) : assert(dividerWidth != null),
|
|
assert(page != null),
|
|
_dividerWidth = dividerWidth,
|
|
_page = page,
|
|
super();
|
|
|
|
final Map<_CupertinoTextSelectionToolbarItemsSlot, RenderBox> slottedChildren = <_CupertinoTextSelectionToolbarItemsSlot, RenderBox>{};
|
|
|
|
RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _CupertinoTextSelectionToolbarItemsSlot slot) {
|
|
if (oldChild != null) {
|
|
dropChild(oldChild);
|
|
slottedChildren.remove(slot);
|
|
}
|
|
if (newChild != null) {
|
|
slottedChildren[slot] = newChild;
|
|
adoptChild(newChild);
|
|
}
|
|
return newChild;
|
|
}
|
|
|
|
bool _isSlottedChild(RenderBox child) {
|
|
return child == _backButton || child == _nextButton || child == _nextButtonDisabled;
|
|
}
|
|
|
|
int _page;
|
|
int get page => _page;
|
|
set page(int value) {
|
|
if (value == _page) {
|
|
return;
|
|
}
|
|
_page = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
double _dividerWidth;
|
|
double get dividerWidth => _dividerWidth;
|
|
set dividerWidth(double value) {
|
|
if (value == _dividerWidth) {
|
|
return;
|
|
}
|
|
_dividerWidth = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
RenderBox? _backButton;
|
|
RenderBox? get backButton => _backButton;
|
|
set backButton(RenderBox? value) {
|
|
_backButton = _updateChild(_backButton, value, _CupertinoTextSelectionToolbarItemsSlot.backButton);
|
|
}
|
|
|
|
RenderBox? _nextButton;
|
|
RenderBox? get nextButton => _nextButton;
|
|
set nextButton(RenderBox? value) {
|
|
_nextButton = _updateChild(_nextButton, value, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
|
|
}
|
|
|
|
RenderBox? _nextButtonDisabled;
|
|
RenderBox? get nextButtonDisabled => _nextButtonDisabled;
|
|
set nextButtonDisabled(RenderBox? value) {
|
|
_nextButtonDisabled = _updateChild(_nextButtonDisabled, value, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
if (firstChild == null) {
|
|
performResize();
|
|
return;
|
|
}
|
|
|
|
// Layout slotted children.
|
|
_backButton!.layout(constraints.loosen(), parentUsesSize: true);
|
|
_nextButton!.layout(constraints.loosen(), parentUsesSize: true);
|
|
_nextButtonDisabled!.layout(constraints.loosen(), parentUsesSize: true);
|
|
|
|
final double subsequentPageButtonsWidth =
|
|
_backButton!.size.width + _nextButton!.size.width;
|
|
double currentButtonPosition = 0.0;
|
|
late double toolbarWidth; // The width of the whole widget.
|
|
late double firstPageWidth;
|
|
int currentPage = 0;
|
|
int i = -1;
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
i++;
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
final ToolbarItemsParentData childParentData =
|
|
child.parentData! as ToolbarItemsParentData;
|
|
childParentData.shouldPaint = false;
|
|
|
|
// Skip slotted children and children on pages after the visible page.
|
|
if (_isSlottedChild(child) || currentPage > _page) {
|
|
return;
|
|
}
|
|
|
|
double paginationButtonsWidth = 0.0;
|
|
if (currentPage == 0) {
|
|
// If this is the last child, it's ok to fit without a forward button.
|
|
paginationButtonsWidth =
|
|
i == childCount - 1 ? 0.0 : _nextButton!.size.width;
|
|
} else {
|
|
paginationButtonsWidth = subsequentPageButtonsWidth;
|
|
}
|
|
|
|
// The width of the menu is set by the first page.
|
|
child.layout(
|
|
BoxConstraints.loose(Size(
|
|
(currentPage == 0 ? constraints.maxWidth : firstPageWidth) - paginationButtonsWidth,
|
|
constraints.maxHeight,
|
|
)),
|
|
parentUsesSize: true,
|
|
);
|
|
|
|
// If this child causes the current page to overflow, move to the next
|
|
// page and relayout the child.
|
|
final double currentWidth =
|
|
currentButtonPosition + paginationButtonsWidth + child.size.width;
|
|
if (currentWidth > constraints.maxWidth) {
|
|
currentPage++;
|
|
currentButtonPosition = _backButton!.size.width + dividerWidth;
|
|
paginationButtonsWidth = _backButton!.size.width + _nextButton!.size.width;
|
|
child.layout(
|
|
BoxConstraints.loose(Size(
|
|
firstPageWidth - paginationButtonsWidth,
|
|
constraints.maxHeight,
|
|
)),
|
|
parentUsesSize: true,
|
|
);
|
|
}
|
|
childParentData.offset = Offset(currentButtonPosition, 0.0);
|
|
currentButtonPosition += child.size.width + dividerWidth;
|
|
childParentData.shouldPaint = currentPage == page;
|
|
|
|
if (currentPage == 0) {
|
|
firstPageWidth = currentButtonPosition + _nextButton!.size.width;
|
|
}
|
|
if (currentPage == page) {
|
|
toolbarWidth = currentButtonPosition;
|
|
}
|
|
});
|
|
|
|
// It shouldn't be possible to navigate beyond the last page.
|
|
assert(page <= currentPage);
|
|
|
|
// Position page nav buttons.
|
|
if (currentPage > 0) {
|
|
final ToolbarItemsParentData nextButtonParentData =
|
|
_nextButton!.parentData! as ToolbarItemsParentData;
|
|
final ToolbarItemsParentData nextButtonDisabledParentData =
|
|
_nextButtonDisabled!.parentData! as ToolbarItemsParentData;
|
|
final ToolbarItemsParentData backButtonParentData =
|
|
_backButton!.parentData! as ToolbarItemsParentData;
|
|
// The forward button always shows if there is more than one page, even on
|
|
// the last page (it's just disabled).
|
|
if (page == currentPage) {
|
|
nextButtonDisabledParentData.offset = Offset(toolbarWidth, 0.0);
|
|
nextButtonDisabledParentData.shouldPaint = true;
|
|
toolbarWidth += nextButtonDisabled!.size.width;
|
|
} else {
|
|
nextButtonParentData.offset = Offset(toolbarWidth, 0.0);
|
|
nextButtonParentData.shouldPaint = true;
|
|
toolbarWidth += nextButton!.size.width;
|
|
}
|
|
if (page > 0) {
|
|
backButtonParentData.offset = Offset.zero;
|
|
backButtonParentData.shouldPaint = true;
|
|
// No need to add the width of the back button to toolbarWidth here. It's
|
|
// already been taken care of when laying out the children to
|
|
// accommodate the back button.
|
|
}
|
|
} else {
|
|
// No divider for the next button when there's only one page.
|
|
toolbarWidth -= dividerWidth;
|
|
}
|
|
|
|
size = constraints.constrain(Size(toolbarWidth, _kToolbarHeight));
|
|
}
|
|
|
|
@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) {
|
|
final Offset childOffset = childParentData.offset + offset;
|
|
context.paintChild(child, childOffset);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderBox child) {
|
|
if (child.parentData is! ToolbarItemsParentData) {
|
|
child.parentData = ToolbarItemsParentData();
|
|
}
|
|
}
|
|
|
|
// Returns true iff the single child is hit by the given position.
|
|
static bool hitTestChild(RenderBox? child, BoxHitTestResult result, { required Offset position }) {
|
|
if (child == null) {
|
|
return false;
|
|
}
|
|
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
|
|
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
|
// Hit test list children.
|
|
// The x, y parameters have the top left of the node's box as the origin.
|
|
RenderBox? child = lastChild;
|
|
while (child != null) {
|
|
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
|
|
|
|
// Don't hit test children that aren't shown.
|
|
if (!childParentData.shouldPaint) {
|
|
child = childParentData.previousSibling;
|
|
continue;
|
|
}
|
|
|
|
if (hitTestChild(child, result, position: position)) {
|
|
return true;
|
|
}
|
|
child = childParentData.previousSibling;
|
|
}
|
|
|
|
// Hit test slot children.
|
|
if (hitTestChild(backButton, result, position: position)) {
|
|
return true;
|
|
}
|
|
if (hitTestChild(nextButton, result, position: position)) {
|
|
return true;
|
|
}
|
|
if (hitTestChild(nextButtonDisabled, result, position: position)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
void attach(PipelineOwner owner) {
|
|
// Attach list children.
|
|
super.attach(owner);
|
|
|
|
// Attach slot children.
|
|
for (final RenderBox child in slottedChildren.values) {
|
|
child.attach(owner);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void detach() {
|
|
// Detach list children.
|
|
super.detach();
|
|
|
|
// Detach slot children.
|
|
for (final RenderBox child in slottedChildren.values) {
|
|
child.detach();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void redepthChildren() {
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
redepthChild(child);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void visitChildren(RenderObjectVisitor visitor) {
|
|
// Visit the slotted children.
|
|
if (_backButton != null) {
|
|
visitor(_backButton!);
|
|
}
|
|
if (_nextButton != null) {
|
|
visitor(_nextButton!);
|
|
}
|
|
if (_nextButtonDisabled != null) {
|
|
visitor(_nextButtonDisabled!);
|
|
}
|
|
// Visit the list children.
|
|
super.visitChildren(visitor);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
List<DiagnosticsNode> debugDescribeChildren() {
|
|
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
|
|
visitChildren((RenderObject renderObjectChild) {
|
|
final RenderBox child = renderObjectChild as RenderBox;
|
|
if (child == backButton) {
|
|
value.add(child.toDiagnosticsNode(name: 'back button'));
|
|
} else if (child == nextButton) {
|
|
value.add(child.toDiagnosticsNode(name: 'next button'));
|
|
} else if (child == nextButtonDisabled) {
|
|
value.add(child.toDiagnosticsNode(name: 'next button disabled'));
|
|
|
|
// List children.
|
|
} else {
|
|
value.add(child.toDiagnosticsNode(name: 'menu item'));
|
|
}
|
|
});
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// The slots that can be occupied by widgets in
|
|
// _CupertinoTextSelectionToolbarItems, excluding the list of children.
|
|
enum _CupertinoTextSelectionToolbarItemsSlot {
|
|
backButton,
|
|
nextButton,
|
|
nextButtonDisabled,
|
|
}
|
|
|
|
/// Text selection controls that follows iOS design conventions.
|
|
final TextSelectionControls cupertinoTextSelectionControls = _CupertinoTextSelectionControls();
|
|
|
|
class _NullElement extends Element {
|
|
_NullElement() : super(_NullWidget());
|
|
|
|
static _NullElement instance = _NullElement();
|
|
|
|
@override
|
|
bool get debugDoingBuild => throw UnimplementedError();
|
|
|
|
@override
|
|
void performRebuild() { }
|
|
}
|
|
|
|
class _NullWidget extends Widget {
|
|
@override
|
|
Element createElement() => throw UnimplementedError();
|
|
}
|