Unverified Commit 0377e80d authored by Tomasz Gucio's avatar Tomasz Gucio Committed by GitHub

Size CupertinoTextSelectionToolbar to children (#133386)

parent 85e52d43
......@@ -14,20 +14,25 @@ import 'colors.dart';
import 'text_selection_toolbar_button.dart';
import 'theme.dart';
// Values extracted from https://developer.apple.com/design/resources/.
// The height of the toolbar, including the arrow.
const double _kToolbarHeight = 45.0;
// The radius of the toolbar RRect shape.
// Value extracted from https://developer.apple.com/design/resources/.
const Radius _kToolbarBorderRadius = Radius.circular(8.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;
// The size of the arrow pointing to the anchor. Eyeballed value.
const Size _kToolbarArrowSize = Size(14.0, 7.0);
// Minimal padding from tip of the selection toolbar arrow to horizontal edges of the
// screen. Eyeballed value.
const double _kArrowScreenPadding = 26.0;
// Values extracted from https://developer.apple.com/design/resources/.
const Radius _kToolbarBorderRadius = Radius.circular(8);
// The size and thickness of the chevron icon used for navigating between toolbar pages.
// Eyeballed values.
const double _kToolbarChevronSize = 10.0;
const double _kToolbarChevronThickness = 2.0;
// Color was measured from a screenshot of iOS 16.0.2
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
......@@ -36,9 +41,6 @@ const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.wit
darkColor: Color(0xFF222222),
);
const double _kToolbarChevronSize = 10;
const double _kToolbarChevronThickness = 2;
// Color was measured from a screenshot of iOS 16.0.2.
const CupertinoDynamicColor _kToolbarDividerColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFFD6D6D6),
......@@ -64,8 +66,8 @@ const Duration _kToolbarTransitionDuration = Duration(milliseconds: 125);
/// Material-style toolbar.
typedef CupertinoToolbarBuilder = Widget Function(
BuildContext context,
Offset anchor,
bool isAbove,
Offset anchorAbove,
Offset anchorBelow,
Widget child,
);
......@@ -127,37 +129,23 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
// Builds a toolbar just like the default iOS toolbar, with the right color
// background and a rounded cutout with an arrow.
static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
final Widget outputChild = _CupertinoTextSelectionToolbarShape(
anchor: anchor,
isAbove: isAbove,
static Widget _defaultToolbarBuilder(
BuildContext context,
Offset anchorAbove,
Offset anchorBelow,
Widget child,
) {
return _CupertinoTextSelectionToolbarShape(
anchorAbove: anchorAbove,
anchorBelow: anchorBelow,
shadowColor: CupertinoTheme.brightnessOf(context) == Brightness.light
? CupertinoColors.black.withOpacity(0.2)
: null,
child: ColoredBox(
color: _kToolbarBackgroundColor.resolveFrom(context),
child: child,
),
);
if (CupertinoTheme.brightnessOf(context) == Brightness.dark) {
return outputChild;
}
return DecoratedBox(
// These shadow values were eyeballed from a screenshot of iOS 16.3.1, as
// light mode didn't appear in the Apple design resources assets linked at
// the top of this file.
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(_kToolbarBorderRadius),
boxShadow: <BoxShadow>[
BoxShadow(
color: CupertinoColors.black.withOpacity(0.2),
blurRadius: 15.0,
offset: Offset(
0.0,
isAbove ? 0.0 : _kToolbarArrowSize.height,
),
),
],
),
child: outputChild,
);
}
@override
......@@ -166,10 +154,6 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context);
final double paddingAbove = mediaQueryPadding.top + kToolbarScreenPadding;
final double toolbarHeightNeeded = paddingAbove
+ _kToolbarContentDistance
+ _kToolbarHeight;
final bool fitsAbove = anchorAbove.dy >= toolbarHeightNeeded;
// The arrow, which points to the anchor, has some margin so it can't get
// too close to the horizontal edges of the screen.
......@@ -196,11 +180,10 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
delegate: TextSelectionToolbarLayoutDelegate(
anchorAbove: anchorAboveAdjusted,
anchorBelow: anchorBelowAdjusted,
fitsAbove: fitsAbove,
),
child: _CupertinoTextSelectionToolbarContent(
anchor: fitsAbove ? anchorAboveAdjusted : anchorBelowAdjusted,
isAbove: fitsAbove,
anchorAbove: anchorAboveAdjusted,
anchorBelow: anchorBelowAdjusted,
toolbarBuilder: toolbarBuilder,
children: children,
),
......@@ -215,30 +198,32 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
// The anchor should be in global coordinates.
class _CupertinoTextSelectionToolbarShape extends SingleChildRenderObjectWidget {
const _CupertinoTextSelectionToolbarShape({
required Offset anchor,
required bool isAbove,
required Offset anchorAbove,
required Offset anchorBelow,
Color? shadowColor,
super.child,
}) : _anchor = anchor,
_isAbove = isAbove;
final Offset _anchor;
}) : _anchorAbove = anchorAbove,
_anchorBelow = anchorBelow,
_shadowColor = shadowColor;
// 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 _isAbove;
final Offset _anchorAbove;
final Offset _anchorBelow;
final Color? _shadowColor;
@override
_RenderCupertinoTextSelectionToolbarShape createRenderObject(BuildContext context) => _RenderCupertinoTextSelectionToolbarShape(
_anchor,
_isAbove,
_anchorAbove,
_anchorBelow,
_shadowColor,
null,
);
@override
void updateRenderObject(BuildContext context, _RenderCupertinoTextSelectionToolbarShape renderObject) {
renderObject
..anchor = _anchor
..isAbove = _isAbove;
..anchorAbove = _anchorAbove
..anchorBelow = _anchorBelow
..shadowColor = _shadowColor;
}
}
......@@ -252,34 +237,47 @@ class _CupertinoTextSelectionToolbarShape extends SingleChildRenderObjectWidget
// on the necessary side.
class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
_RenderCupertinoTextSelectionToolbarShape(
this._anchor,
this._isAbove,
this._anchorAbove,
this._anchorBelow,
this._shadowColor,
super.child,
);
@override
bool get isRepaintBoundary => true;
Offset get anchor => _anchor;
Offset _anchor;
set anchor(Offset value) {
if (value == _anchor) {
Offset get anchorAbove => _anchorAbove;
Offset _anchorAbove;
set anchorAbove(Offset value) {
if (value == _anchorAbove) {
return;
}
_anchor = value;
_anchorAbove = value;
markNeedsLayout();
}
bool get isAbove => _isAbove;
bool _isAbove;
set isAbove(bool value) {
if (_isAbove == value) {
Offset get anchorBelow => _anchorBelow;
Offset _anchorBelow;
set anchorBelow(Offset value) {
if (value == _anchorBelow) {
return;
}
_isAbove = value;
_anchorBelow = value;
markNeedsLayout();
}
Color? get shadowColor => _shadowColor;
Color? _shadowColor;
set shadowColor(Color? value) {
if (value == _shadowColor) {
return;
}
_shadowColor = value;
markNeedsPaint();
}
bool get isAbove => anchorAbove.dy >= (child?.size.height ?? 0.0) - _kToolbarArrowSize.height * 2;
@override
void performLayout() {
final RenderBox? child = this.child;
......@@ -287,26 +285,21 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
return;
}
// The child is tall enough to have the arrow clipped out of it on both sides
// top and bottom. Since _kToolbarHeight includes the height of one arrow, the
// total height that the child is given is that plus one more arrow height.
// The extra height on the opposite side of the arrow will be clipped out. By
// using this approach, the buttons don't need any special padding that
// depends on isAbove.
final BoxConstraints heightConstraint = BoxConstraints(
minHeight: _kToolbarHeight + _kToolbarArrowSize.height,
maxHeight: _kToolbarHeight + _kToolbarArrowSize.height,
final BoxConstraints enforcedConstraint = BoxConstraints(
minWidth: _kToolbarArrowSize.width + _kToolbarBorderRadius.x * 2,
).enforce(constraints.loosen());
child.layout(enforcedConstraint, parentUsesSize: true);
child.layout(heightConstraint, parentUsesSize: true);
// The buttons are padded on both top and bottom sufficiently to have
// the arrow clipped out of it on either side. By
// using this approach, the buttons don't need any special padding that
// depends on isAbove.
// The height of one arrow will be clipped off of the child, so adjust the
// size and position to remove that piece from the layout.
final BoxParentData childParentData = child.parentData! as BoxParentData;
childParentData.offset = Offset(
0.0,
_isAbove ? -_kToolbarArrowSize.height : 0.0,
isAbove ? -_kToolbarArrowSize.height : 0.0,
);
size = Size(
child.size.width,
......@@ -314,6 +307,13 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
);
}
// Returns the RRect inside which the child is painted.
RRect _shapeRRect(RenderBox child) {
final Rect rect = Offset(0.0, _kToolbarArrowSize.height)
& Size(child.size.width, child.size.height - _kToolbarArrowSize.height * 2);
return RRect.fromRectAndRadius(rect, _kToolbarBorderRadius).scaleRadii();
}
// Adds the given `rrect` to the current `path`, starting from the last point
// in `path` and ends after the last corner of the rrect (closest corner to
// `startAngle` in the counterclockwise direction), without closing the path.
......@@ -328,7 +328,7 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
// added, then this method returns the mutated path without closing it.
static Path _addRRectToPath(Path path, RRect rrect, { required double startAngle }) {
const double halfPI = math.pi / 2;
assert(startAngle % halfPI == 0);
assert(startAngle % halfPI == 0.0);
final Rect rect = rrect.outerRect;
final List<(Offset, Radius)> rrectCorners = <(Offset, Radius)>[
......@@ -351,12 +351,8 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
return path;
}
// The path is described in the toolbar's coordinate system.
Path _clipPath(RenderBox child) {
final Rect rect = Offset(0.0, _isAbove ? 0 : _kToolbarArrowSize.height)
& Size(size.width, size.height - _kToolbarArrowSize.height);
final RRect rrect = RRect.fromRectAndRadius(rect, _kToolbarBorderRadius).scaleRadii();
// The path is described in the toolbar child's coordinate system.
Path _clipPath(RenderBox child, RRect rrect) {
final Path path = Path();
// If there isn't enough width for the arrow + radii, ignore the arrow.
// Because of the constraints we gave children in performLayout, this should
......@@ -366,7 +362,7 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
return path..addRRect(rrect);
}
final Offset localAnchor = globalToLocal(_anchor);
final Offset localAnchor = globalToLocal(isAbove ? _anchorAbove : _anchorBelow);
final double arrowTipX = clampDouble(
localAnchor.dx,
_kToolbarBorderRadius.x + _kToolbarArrowSize.width / 2,
......@@ -374,18 +370,22 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
);
// Draw the path clockwise, starting from the beginning side of the arrow.
if (_isAbove) {
if (isAbove) {
final double arrowBaseY = child.size.height - _kToolbarArrowSize.height;
final double arrowTipY = child.size.height;
path
..moveTo(arrowTipX + _kToolbarArrowSize.width / 2, rect.bottom) // right side of the arrow triangle
..lineTo(arrowTipX, rect.bottom + _kToolbarArrowSize.height) // The tip of the arrow
..lineTo(arrowTipX - _kToolbarArrowSize.width / 2, rect.bottom); // left side of the arrow triangle
..moveTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBaseY) // right side of the arrow triangle
..lineTo(arrowTipX, arrowTipY) // The tip of the arrow
..lineTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBaseY); // left side of the arrow triangle
} else {
final double arrowBaseY = _kToolbarArrowSize.height;
const double arrowTipY = 0.0;
path
..moveTo(arrowTipX - _kToolbarArrowSize.width / 2, rect.top) // right side of the arrow triangle
..lineTo(arrowTipX, rect.top) // The tip of the arrow
..lineTo(arrowTipX + _kToolbarArrowSize.width / 2, rect.top); // left side of the arrow triangle
..moveTo(arrowTipX - _kToolbarArrowSize.width / 2, arrowBaseY) // right side of the arrow triangle
..lineTo(arrowTipX, arrowTipY) // The tip of the arrow
..lineTo(arrowTipX + _kToolbarArrowSize.width / 2, arrowBaseY); // left side of the arrow triangle
}
final double startAngle = _isAbove ? math.pi / 2 : -math.pi / 2;
final double startAngle = isAbove ? math.pi / 2 : -math.pi / 2;
return _addRRectToPath(path, rrect, startAngle: startAngle)..close();
}
......@@ -395,12 +395,34 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
if (child == null) {
return;
}
final BoxParentData childParentData = child.parentData! as BoxParentData;
final RRect rrect = _shapeRRect(child);
final Path clipPath = _clipPath(child, rrect);
// If configured, paint the shadow beneath the shape.
if (_shadowColor != null) {
final BoxShadow boxShadow = BoxShadow(
color: _shadowColor!,
blurRadius: 15.0,
);
final RRect shadowRRect = RRect.fromLTRBR(
rrect.left,
rrect.top,
rrect.right,
rrect.bottom + _kToolbarArrowSize.height,
_kToolbarBorderRadius,
).shift(offset + childParentData.offset + boxShadow.offset);
context.canvas.drawRRect(shadowRRect, boxShadow.toPaint());
}
_clipPathLayer.layer = context.pushClipPath(
needsCompositing,
offset,
Offset.zero & size,
_clipPath(child),
super.paint,
offset + childParentData.offset,
Offset.zero & child.size,
clipPath,
(PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child, innerOffset),
oldLayer: _clipPathLayer.layer,
);
}
......@@ -434,21 +456,27 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
..style = PaintingStyle.stroke;
final BoxParentData childParentData = child.parentData! as BoxParentData;
context.canvas.drawPath(_clipPath(child).shift(offset + childParentData.offset), debugPaint);
final Path clipPath = _clipPath(child, _shapeRRect(child));
context.canvas.drawPath(clipPath.shift(offset + childParentData.offset), debugPaint);
return true;
}());
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
final RenderBox? child = this.child;
if (child == null) {
return false;
}
// Positions outside of the clipped area of the child are not counted as
// hits.
final BoxParentData childParentData = child!.parentData! as BoxParentData;
final BoxParentData childParentData = child.parentData! as BoxParentData;
final Rect hitBox = Rect.fromLTWH(
childParentData.offset.dx,
childParentData.offset.dy + _kToolbarArrowSize.height,
child!.size.width,
child!.size.height - _kToolbarArrowSize.height * 2,
child.size.width,
child.size.height - _kToolbarArrowSize.height * 2,
);
if (!hitBox.contains(position)) {
return false;
......@@ -465,15 +493,15 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
// The anchor should be in global coordinates.
class _CupertinoTextSelectionToolbarContent extends StatefulWidget {
const _CupertinoTextSelectionToolbarContent({
required this.anchor,
required this.isAbove,
required this.anchorAbove,
required this.anchorBelow,
required this.toolbarBuilder,
required this.children,
}) : assert(children.length > 0);
final Offset anchor;
final Offset anchorAbove;
final Offset anchorBelow;
final List<Widget> children;
final bool isAbove;
final CupertinoToolbarBuilder toolbarBuilder;
@override
......@@ -564,26 +592,48 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel
super.dispose();
}
Widget _createChevron({required bool isLeft}) {
final Color color = _kToolbarTextColor.resolveFrom(context);
return IgnorePointer(
child: Center(
// If widthFactor is not set to 0, the button is given unbounded width.
widthFactor: 0,
@override
Widget build(BuildContext context) {
final Color chevronColor = _kToolbarTextColor.resolveFrom(context);
// Wrap the children and the chevron painters in Center with widthFactor
// and heightFactor of 1.0 so _CupertinoTextSelectionToolbarItems can get
// the natural size of the buttons and then expand vertically as needed.
final Widget backButton = Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: CupertinoTextSelectionToolbarButton(
onPressed: _handlePreviousPage,
child: IgnorePointer(
child: CustomPaint(
painter: isLeft
? _LeftCupertinoChevronPainter(color: color)
: _RightCupertinoChevronPainter(color: color),
painter: _LeftCupertinoChevronPainter(color: chevronColor),
size: const Size.square(_kToolbarChevronSize),
),
),
),
);
}
final Widget nextButton = Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: CupertinoTextSelectionToolbarButton(
onPressed: _handleNextPage,
child: IgnorePointer(
child: CustomPaint(
painter: _RightCupertinoChevronPainter(color: chevronColor),
size: const Size.square(_kToolbarChevronSize),
),
),
),
);
final List<Widget> children = widget.children.map((Widget child) {
return Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: child,
);
}).toList();
@override
Widget build(BuildContext context) {
return widget.toolbarBuilder(context, widget.anchor, widget.isAbove, FadeTransition(
return widget.toolbarBuilder(context, widget.anchorAbove, widget.anchorBelow, FadeTransition(
opacity: _controller,
child: AnimatedSize(
duration: _kToolbarTransitionDuration,
......@@ -593,17 +643,11 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel
child: _CupertinoTextSelectionToolbarItems(
key: _toolbarItemsKey,
page: _page,
backButton: CupertinoTextSelectionToolbarButton(
onPressed: _handlePreviousPage,
child: _createChevron(isLeft: true),
),
backButton: backButton,
dividerColor: _kToolbarDividerColor.resolveFrom(context),
dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context),
nextButton: CupertinoTextSelectionToolbarButton(
onPressed: _handleNextPage,
child: _createChevron(isLeft: false),
),
children: widget.children,
nextButton: nextButton,
children: children,
),
),
),
......@@ -633,7 +677,7 @@ abstract class _CupertinoChevronPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
assert(size.height == size.width, 'size must have the same height and width');
assert(size.height == size.width, 'size must have the same height and width: $size');
final double iconSize = size.height;
......@@ -893,10 +937,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
return newChild;
}
bool _isSlottedChild(RenderBox child) {
return child == _backButton || child == _nextButton;
}
int _page;
int get page => _page;
set page(int value) {
......@@ -946,66 +986,71 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
return;
}
// First pass: determine the height of the tallest child.
double greatestHeight = 0.0;
visitChildren((RenderObject renderObjectChild) {
final RenderBox child = renderObjectChild as RenderBox;
final double childHeight = child.getMaxIntrinsicHeight(constraints.maxWidth);
if (childHeight > greatestHeight) {
greatestHeight = childHeight;
}
});
// Layout slotted children.
_backButton!.layout(constraints.loosen(), parentUsesSize: true);
_nextButton!.layout(constraints.loosen(), parentUsesSize: true);
final BoxConstraints slottedConstraints = BoxConstraints(
maxWidth: constraints.maxWidth,
minHeight: greatestHeight,
maxHeight: greatestHeight,
);
_backButton!.layout(slottedConstraints, parentUsesSize: true);
_nextButton!.layout(slottedConstraints, parentUsesSize: true);
final double subsequentPageButtonsWidth =
_backButton!.size.width + _nextButton!.size.width;
final double subsequentPageButtonsWidth = _backButton!.size.width + _nextButton!.size.width;
double currentButtonPosition = 0.0;
late double toolbarWidth; // The width of the whole widget.
late double greatestHeight = 0.0;
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;
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) {
if (child == _backButton || child == _nextButton || 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.
// If this is the last child on the first page, it's ok to fit without a forward button.
// Note childCount doesn't include slotted children which come before the list ones.
paginationButtonsWidth =
i == childCount + 1 ? 0.0 : _nextButton!.size.width;
} else {
paginationButtonsWidth = subsequentPageButtonsWidth;
}
double paginationButtonsWidth = currentPage == 0
? i == childCount + 1 ? 0.0 : _nextButton!.size.width
: 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,
)),
BoxConstraints(
maxWidth: (currentPage == 0 ? constraints.maxWidth : firstPageWidth) - paginationButtonsWidth,
minHeight: greatestHeight,
maxHeight: greatestHeight,
),
parentUsesSize: true,
);
greatestHeight = child.size.height > greatestHeight
? child.size.height
: greatestHeight;
// 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;
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,
)),
BoxConstraints(
maxWidth: firstPageWidth - paginationButtonsWidth,
minHeight: greatestHeight,
maxHeight: greatestHeight,
),
parentUsesSize: true,
);
}
......@@ -1026,10 +1071,8 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
// Position page nav buttons.
if (currentPage > 0) {
final ToolbarItemsParentData nextButtonParentData =
_nextButton!.parentData! as ToolbarItemsParentData;
final ToolbarItemsParentData backButtonParentData =
_backButton!.parentData! as ToolbarItemsParentData;
final ToolbarItemsParentData nextButtonParentData = _nextButton!.parentData! as ToolbarItemsParentData;
final ToolbarItemsParentData backButtonParentData = _backButton!.parentData! as ToolbarItemsParentData;
// The forward button only shows when there's a page after this one.
if (page != currentPage) {
nextButtonParentData.offset = Offset(toolbarWidth, 0.0);
......@@ -1043,15 +1086,15 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
// 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;
}
// Update previous/next page values so that we can check in the horizontal
// drag gesture callback if it's possible to navigate.
hasNextPage = page != currentPage;
hasPreviousPage = page > 0;
} else {
// No divider for the next button when there's only one page.
toolbarWidth -= dividerWidth;
}
size = constraints.constrain(Size(toolbarWidth, greatestHeight));
}
......@@ -1093,8 +1136,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
if (child == null) {
return false;
}
final ToolbarItemsParentData childParentData =
child.parentData! as ToolbarItemsParentData;
final ToolbarItemsParentData childParentData = child.parentData! as ToolbarItemsParentData;
if (!childParentData.shouldPaint) {
return false;
}
......
......@@ -6837,7 +6837,7 @@ void main() {
includes: <Offset> [
// Expected center of the arrow. The arrow should stay clear of
// the edges of the selection toolbar.
Offset(26.0, bottomLeftSelectionPosition.dy + 7.0 + 8.0 + 0.1),
Offset(26.0, bottomLeftSelectionPosition.dy + 8.0 + 0.1),
],
),
),
......@@ -6847,10 +6847,10 @@ void main() {
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 7 + 8, epsilon: 0.01),
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
leftMatcher: moreOrLessEquals(8),
rightMatcher: lessThanOrEqualTo(400 - 8),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 45, epsilon: 0.01),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01),
),
),
);
......@@ -6898,7 +6898,7 @@ void main() {
],
includes: <Offset> [
// Expected center of the arrow.
Offset(400 - 26.0, bottomLeftSelectionPosition.dy + 7 + 8 + 0.1),
Offset(400 - 26.0, bottomLeftSelectionPosition.dy + 8 + 0.1),
],
),
),
......@@ -6908,9 +6908,9 @@ void main() {
find.byType(CupertinoTextSelectionToolbar),
paints..clipPath(
pathMatcher: PathBoundsMatcher(
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 7 + 8, epsilon: 0.01),
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
rightMatcher: moreOrLessEquals(400.0 - 8),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 45, epsilon: 0.01),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 44, epsilon: 0.01),
leftMatcher: greaterThanOrEqualTo(8),
),
),
......@@ -6963,7 +6963,7 @@ void main() {
paints..clipPath(
pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01),
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8),
),
......@@ -7032,7 +7032,7 @@ void main() {
paints..clipPath(
pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8),
),
......@@ -7105,7 +7105,7 @@ void main() {
paints..clipPath(
pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 44, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8),
),
......
......@@ -671,8 +671,8 @@ void main() {
final Offset textFieldOffset =
tester.getTopLeft(find.byType(CupertinoTextField));
// 7.0 + 45.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding
expect(selectionOffset.dy + 7.0 + 45.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
// 7.0 + 44.0 + 8.0 - 8.0 = _kToolbarArrowSize + text_button_height + _kToolbarContentDistance - padding
expect(selectionOffset.dy + 7.0 + 44.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
},
skip: isBrowser, // [intended] the selection menu isn't required by web
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
......
......@@ -12,7 +12,7 @@ import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
// These constants are copied from cupertino/text_selection_toolbar.dart.
const double _kArrowScreenPadding = 26.0;
const double _kToolbarContentDistance = 8.0;
const double _kToolbarHeight = 45.0;
const Size _kToolbarArrowSize = Size(14.0, 7.0);
// A custom text selection menu that just displays a single custom button.
class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionControls {
......@@ -271,7 +271,7 @@ void main() {
testWidgetsWithLeakTracking('positions itself at anchorAbove if it fits', (WidgetTester tester) async {
late StateSetter setState;
const double height = _kToolbarHeight;
const double height = 50.0;
const double anchorBelowY = 500.0;
double anchorAboveY = 0.0;
const double paddingAbove = 12.0;
......@@ -332,7 +332,7 @@ void main() {
});
await tester.pump();
toolbarY = tester.getTopLeft(findToolbar()).dy;
expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance));
expect(toolbarY, equals(anchorAboveY - height + _kToolbarArrowSize.height - _kToolbarContentDistance));
}, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web.
testWidgetsWithLeakTracking('can create and use a custom toolbar', (WidgetTester tester) async {
......@@ -429,7 +429,7 @@ void main() {
testWidgetsWithLeakTracking('draws a shadow below the toolbar in light mode', (WidgetTester tester) async {
late StateSetter setState;
const double height = _kToolbarHeight;
const double height = 50.0;
double anchorAboveY = 0.0;
await tester.pumpWidget(
......@@ -468,20 +468,15 @@ void main() {
),
);
// When the toolbar is below the content, the shadow hangs below the entire
// toolbar.
final Finder finder = find.descendant(
of: find.byType(CupertinoTextSelectionToolbar),
matching: find.byType(DecoratedBox),
final double dividerWidth = 1.0 / tester.view.devicePixelRatio;
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..rrect(
rrect: RRect.fromLTRBR(8.0, 515.0, 158.0 + 2 * dividerWidth, 558.0, const Radius.circular(8.0)),
color: const Color(0x33000000),
),
);
expect(finder, findsOneWidget);
DecoratedBox decoratedBox = tester.widget(finder.first);
BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
List<BoxShadow>? shadows = boxDecoration.boxShadow;
expect(shadows, isNotNull);
expect(shadows, hasLength(1));
BoxShadow shadow = boxDecoration.boxShadow!.first;
expect(shadow.offset.dy, equals(7.0));
// When the toolbar is above the content, the shadow sits around the arrow
// with no offset.
......@@ -489,12 +484,13 @@ void main() {
anchorAboveY = 80.0;
});
await tester.pump();
decoratedBox = tester.widget(finder.first);
boxDecoration = decoratedBox.decoration as BoxDecoration;
shadows = boxDecoration.boxShadow;
expect(shadows, isNotNull);
expect(shadows, hasLength(1));
shadow = boxDecoration.boxShadow!.first;
expect(shadow.offset.dy, equals(0.0));
expect(
find.byType(CupertinoTextSelectionToolbar),
paints..rrect(
rrect: RRect.fromLTRBR(8.0, 29.0, 158.0 + 2 * dividerWidth, 72.0, const Radius.circular(8.0)),
color: const Color(0x33000000),
),
);
}, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web.
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment