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