Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
26021191
Unverified
Commit
26021191
authored
Jun 19, 2019
by
LongCatIsLooong
Committed by
GitHub
Jun 19, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Cupertino text edit tooltip rework (#34095)
parent
7472fad1
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
723 additions
and
218 deletions
+723
-218
text_selection.dart
packages/flutter/lib/src/cupertino/text_selection.dart
+249
-197
text_selection.dart
packages/flutter/lib/src/material/text_selection.dart
+1
-0
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+22
-7
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+438
-6
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+4
-6
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+1
-1
matchers.dart
packages/flutter_test/lib/src/matchers.dart
+8
-1
No files found.
packages/flutter/lib/src/cupertino/text_selection.dart
View file @
26021191
...
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/rendering.dart'
;
...
...
@@ -11,219 +12,238 @@ import 'button.dart';
import
'colors.dart'
;
import
'localizations.dart'
;
// Minimal padding from all edges of the selection toolbar to all edges of the
// viewport.
const
double
_kToolbarScreenPadding
=
8.0
;
const
double
_kToolbarHeight
=
36.0
;
const
Color
_kToolbarBackgroundColor
=
Color
(
0xFF2E2E2E
);
const
Color
_kToolbarDividerColor
=
Color
(
0xFFB9B9B9
);
// Read off from the output on iOS 12. This color does not vary with the
// application's theme color.
const
Color
_kHandlesColor
=
Color
(
0xFF136FE0
);
const
double
_kSelectionHandleOverlap
=
1.5
;
const
double
_kSelectionHandleRadius
=
5.5
;
const
Size
_kToolbarTriangleSize
=
Size
(
18.0
,
9.0
);
// 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
// The height of the toolbar, including the arrow.
const
double
_kToolbarHeight
=
43.0
;
const
Color
_kToolbarBackgroundColor
=
Color
(
0xEB202020
);
const
Color
_kToolbarDividerColor
=
Color
(
0xFF808080
);
const
Size
_kToolbarArrowSize
=
Size
(
14.0
,
7.0
);
const
EdgeInsets
_kToolbarButtonPadding
=
EdgeInsets
.
symmetric
(
vertical:
10.0
,
horizontal:
18.0
);
const
BorderRadius
_kToolbarBorderRadius
=
BorderRadius
.
all
(
Radius
.
circular
(
7.5
)
);
const
Radius
_kToolbarBorderRadius
=
Radius
.
circular
(
8
);
const
TextStyle
_kToolbarButtonFontStyle
=
TextStyle
(
inherit:
false
,
fontSize:
14.0
,
letterSpacing:
-
0.1
1
,
fontWeight:
FontWeight
.
w
3
00
,
letterSpacing:
-
0.1
5
,
fontWeight:
FontWeight
.
w
4
00
,
color:
CupertinoColors
.
white
,
);
/// The direction of the triangle attached to the toolbar.
/// 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:
///
/// Defaults to showing the triangle downwards if sufficient space is available
/// to show the toolbar above the text field. Otherwise, the toolbar will
/// appear below the text field and the triangle's direction will be [up].
enum
_ArrowDirection
{
up
,
down
}
/// * [TextSelectionControls.buildToolbar], where [CupertinoTextSelectionToolbar]
/// will be used to build an iOS-style toolbar.
@visibleForTesting
class
CupertinoTextSelectionToolbar
extends
SingleChildRenderObjectWidget
{
const
CupertinoTextSelectionToolbar
.
_
({
Key
key
,
double
barTopY
,
double
arrowTipX
,
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
;
/// Paints a triangle below the toolbar.
class
_TextSelectionToolbarNotchPainter
extends
CustomPainter
{
const
_TextSelectionToolbarNotchPainter
(
this
.
arrowDirection
)
:
assert
(
arrowDirection
!=
null
);
// The y-coordinate of the tip of the arrow, in global coordinate system.
final
double
_arrowTipX
;
final
_ArrowDirection
arrowDirection
;
// 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
void
paint
(
Canvas
canvas
,
Size
size
)
{
final
Paint
paint
=
Paint
()
..
color
=
_kToolbarBackgroundColor
..
style
=
PaintingStyle
.
fill
;
final
double
triangleBottomY
=
(
arrowDirection
==
_ArrowDirection
.
down
)
?
0.0
:
_kToolbarTriangleSize
.
height
;
final
Path
triangle
=
Path
()
..
lineTo
(
_kToolbarTriangleSize
.
width
/
2
,
triangleBottomY
)
..
lineTo
(
0.0
,
_kToolbarTriangleSize
.
height
)
..
lineTo
(-(
_kToolbarTriangleSize
.
width
/
2
),
triangleBottomY
)
..
close
();
canvas
.
drawPath
(
triangle
,
paint
);
_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
bool
shouldRepaint
(
_TextSelectionToolbarNotchPainter
oldPainter
)
=>
false
;
String
toString
()
=>
'offset=
$offset
, arrowXOffsetFromCenter=
$arrowXOffsetFromCenter
'
;
}
/// Manages a copy/paste text selection toolbar.
class
_TextSelectionToolbar
extends
StatelessWidget
{
const
_TextSelectionToolbar
({
Key
key
,
this
.
handleCut
,
this
.
handleCopy
,
this
.
handlePaste
,
this
.
handleSelectAll
,
this
.
arrowDirection
,
})
:
super
(
key:
key
);
final
VoidCallback
handleCut
;
final
VoidCallback
handleCopy
;
final
VoidCallback
handlePaste
;
final
VoidCallback
handleSelectAll
;
final
_ArrowDirection
arrowDirection
;
class
_ToolbarRenderBox
extends
RenderShiftedBox
{
_ToolbarRenderBox
(
this
.
_barTopY
,
this
.
_arrowTipX
,
this
.
_isArrowPointingDown
,
RenderBox
child
,
)
:
super
(
child
);
@override
Widget
build
(
BuildContext
context
)
{
final
List
<
Widget
>
items
=
<
Widget
>[];
final
Widget
onePhysicalPixelVerticalDivider
=
SizedBox
(
width:
1.0
/
MediaQuery
.
of
(
context
).
devicePixelRatio
);
final
CupertinoLocalizations
localizations
=
CupertinoLocalizations
.
of
(
context
);
bool
get
isRepaintBoundary
=>
true
;
if
(
handleCut
!=
null
)
items
.
add
(
_buildToolbarButton
(
localizations
.
cutButtonLabel
,
handleCut
));
double
_barTopY
;
set
barTopY
(
double
value
)
{
if
(
_barTopY
==
value
)
{
return
;
}
_barTopY
=
value
;
markNeedsLayout
();
markNeedsSemanticsUpdate
();
}
if
(
handleCopy
!=
null
)
{
if
(
items
.
isNotEmpty
)
items
.
add
(
onePhysicalPixelVerticalDivider
);
items
.
add
(
_buildToolbarButton
(
localizations
.
copyButtonLabel
,
handleCopy
));
double
_arrowTipX
;
set
arrowTipX
(
double
value
)
{
if
(
_arrowTipX
==
value
)
{
return
;
}
_arrowTipX
=
value
;
markNeedsLayout
();
markNeedsSemanticsUpdate
();
}
if
(
handlePaste
!=
null
)
{
if
(
items
.
isNotEmpty
)
items
.
add
(
onePhysicalPixelVerticalDivider
);
items
.
add
(
_buildToolbarButton
(
localizations
.
pasteButtonLabel
,
handlePaste
));
bool
_isArrowPointingDown
;
set
isArrowPointingDown
(
bool
value
)
{
if
(
_isArrowPointingDown
==
value
)
{
return
;
}
_isArrowPointingDown
=
value
;
markNeedsLayout
();
markNeedsSemanticsUpdate
();
}
if
(
handleSelectAll
!=
null
)
{
if
(
items
.
isNotEmpty
)
items
.
add
(
onePhysicalPixelVerticalDivider
);
items
.
add
(
_buildToolbarButton
(
localizations
.
selectAllButtonLabel
,
handleSelectAll
));
final
BoxConstraints
heightConstraint
=
const
BoxConstraints
.
tightFor
(
height:
_kToolbarHeight
);
@override
void
setupParentData
(
RenderObject
child
)
{
if
(
child
.
parentData
is
!
_ToolbarParentData
)
{
child
.
parentData
=
_ToolbarParentData
();
}
// If there is no option available, build an empty widget.
if
(
items
.
isEmpty
)
{
return
Container
(
width:
0.0
,
height:
0.0
);
}
const
Widget
padding
=
Padding
(
padding:
EdgeInsets
.
only
(
bottom:
10.0
));
@override
void
performLayout
()
{
size
=
constraints
.
biggest
;
final
Widget
triangle
=
SizedBox
.
fromSize
(
size:
_kToolbarTriangleSize
,
child:
CustomPaint
(
painter:
_TextSelectionToolbarNotchPainter
(
arrowDirection
),
),
);
if
(
child
==
null
)
{
return
;
}
final
BoxConstraints
enforcedConstraint
=
constraints
.
deflate
(
const
EdgeInsets
.
symmetric
(
horizontal:
_kToolbarScreenPadding
))
.
loosen
(
);
final
Widget
toolbar
=
ClipRRect
(
borderRadius:
_kToolbarBorderRadius
,
child:
DecoratedBox
(
decoration:
BoxDecoration
(
color:
_kToolbarDividerColor
,
borderRadius:
_kToolbarBorderRadius
,
// Add a hairline border with the button color to avoid
// antialiasing artifacts.
border:
Border
.
all
(
color:
_kToolbarBackgroundColor
,
width:
0
),
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
items
),
),
);
child
.
layout
(
heightConstraint
.
enforce
(
enforcedConstraint
),
parentUsesSize:
true
,);
final
_ToolbarParentData
childParentData
=
child
.
parentData
;
final
List
<
Widget
>
menus
=
(
arrowDirection
==
_ArrowDirection
.
down
)
?
<
Widget
>[
toolbar
,
// TODO(xster): Position the triangle based on the layout delegate, and
// avoid letting the triangle line up with any dividers.
// https://github.com/flutter/flutter/issues/11274
triangle
,
padding
,
]
:
<
Widget
>[
padding
,
triangle
,
toolbar
,
];
return
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
menus
,
);
final
Offset
localTopCenter
=
globalToLocal
(
Offset
(
_arrowTipX
,
_barTopY
));
// 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
=
localTopCenter
.
dx
.
clamp
(
lowerBound
,
upperBound
);
childParentData
.
offset
=
Offset
(
adjustedCenterX
-
child
.
size
.
width
/
2
,
localTopCenter
.
dy
);
childParentData
.
arrowXOffsetFromCenter
=
localTopCenter
.
dx
-
adjustedCenterX
;
}
//
/ Builds a themed [CupertinoButton] for the toolbar
.
CupertinoButton
_buildToolbarButton
(
String
text
,
VoidCallback
onPressed
)
{
return
CupertinoButton
(
child:
Text
(
text
,
style:
_kToolbarButtonFontStyle
),
color:
_kToolbarBackgroundColor
,
minSize:
_kToolbarHeight
,
padding:
_kToolbarButtonPadding
,
borderRadius:
null
,
pressedOpacity:
0.7
,
onPressed:
onPressed
,
//
The path is described in the toolbar's coordinate system
.
Path
_clipPath
(
)
{
final
_ToolbarParentData
childParentData
=
child
.
parentData
;
final
Path
rrect
=
Path
()
..
addRRect
(
RRect
.
fromRectAndRadius
(
Offset
(
0
,
_isArrowPointingDown
?
0
:
_kToolbarArrowSize
.
height
,)
&
Size
(
child
.
size
.
width
,
child
.
size
.
height
-
_kToolbarArrowSize
.
height
)
,
_kToolbarBorderRadius
,
)
,
);
}
}
/// Centers the toolbar around the given position, ensuring that it remains on
/// screen.
class
_TextSelectionToolbarLayout
extends
SingleChildLayoutDelegate
{
_TextSelectionToolbarLayout
(
this
.
screenSize
,
this
.
globalEditableRegion
,
this
.
position
);
final
double
arrowTipX
=
child
.
size
.
width
/
2
+
childParentData
.
arrowXOffsetFromCenter
;
/// The size of the screen at the time that the toolbar was last laid out.
final
Size
screenSize
;
final
double
arrowBottomY
=
_isArrowPointingDown
?
child
.
size
.
height
-
_kToolbarArrowSize
.
height
:
_kToolbarArrowSize
.
height
;
/// Size and position of the editing region at the time the toolbar was last
/// laid out, in global coordinates.
final
Rect
globalEditableRegion
;
final
double
arrowTipY
=
_isArrowPointingDown
?
child
.
size
.
height
:
0
;
/// Anchor position of the toolbar, relative to the top left of the
/// [globalEditableRegion].
final
Offset
position
;
final
Path
arrow
=
Path
()
..
moveTo
(
arrowTipX
,
arrowTipY
)
..
lineTo
(
arrowTipX
-
_kToolbarArrowSize
.
width
/
2
,
arrowBottomY
)
..
lineTo
(
arrowTipX
+
_kToolbarArrowSize
.
width
/
2
,
arrowBottomY
)
..
close
();
@override
BoxConstraints
getConstraintsForChild
(
BoxConstraints
constraints
)
{
return
constraints
.
loosen
();
return
Path
.
combine
(
PathOperation
.
union
,
rrect
,
arrow
);
}
@override
Offset
getPositionForChild
(
Size
size
,
Size
childSize
)
{
final
Offset
globalPosition
=
globalEditableRegion
.
topLeft
+
position
;
double
x
=
globalPosition
.
dx
-
childSize
.
width
/
2.0
;
double
y
=
globalPosition
.
dy
-
childSize
.
height
;
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
if
(
child
==
null
)
{
return
;
}
if
(
x
<
_kToolbarScreenPadding
)
x
=
_kToolbarScreenPadding
;
else
if
(
x
+
childSize
.
width
>
screenSize
.
width
-
_kToolbarScreenPadding
)
x
=
screenSize
.
width
-
childSize
.
width
-
_kToolbarScreenPadding
;
final
_ToolbarParentData
childParentData
=
child
.
parentData
;
context
.
pushClipPath
(
needsCompositing
,
offset
+
childParentData
.
offset
,
Offset
.
zero
&
child
.
size
,
_clipPath
(),
(
PaintingContext
innerContext
,
Offset
innerOffset
)
=>
innerContext
.
paintChild
(
child
,
innerOffset
),
);
}
if
(
y
<
_kToolbarScreenPadding
)
y
=
_kToolbarScreenPadding
;
else
if
(
y
+
childSize
.
height
>
screenSize
.
height
-
_kToolbarScreenPadding
)
y
=
screenSize
.
height
-
childSize
.
height
-
_kToolbarScreenPadding
;
Paint
_debugPaint
;
return
Offset
(
x
,
y
);
@override
void
debugPaintSize
(
PaintingContext
context
,
Offset
offset
)
{
assert
(()
{
if
(
child
==
null
)
{
return
true
;
}
@override
bool
shouldRelayout
(
_TextSelectionToolbarLayout
oldDelegate
)
{
return
screenSize
!=
oldDelegate
.
screenSize
||
globalEditableRegion
!=
oldDelegate
.
globalEditableRegion
||
position
!=
oldDelegate
.
position
;
_debugPaint
??=
Paint
()
..
shader
=
ui
.
Gradient
.
linear
(
const
Offset
(
0.0
,
0.0
),
const
Offset
(
10.0
,
10.0
),
<
Color
>[
const
Color
(
0x00000000
),
const
Color
(
0xFFFF00FF
),
const
Color
(
0xFFFF00FF
),
const
Color
(
0x00000000
)],
<
double
>[
0.25
,
0.25
,
0.75
,
0.75
],
TileMode
.
repeated
,
)
..
strokeWidth
=
2.0
..
style
=
PaintingStyle
.
stroke
;
final
_ToolbarParentData
childParentData
=
child
.
parentData
;
context
.
canvas
.
drawPath
(
_clipPath
().
shift
(
offset
+
childParentData
.
offset
),
_debugPaint
);
return
true
;
}());
}
}
...
...
@@ -274,47 +294,79 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
)
{
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
bool
isArrowPointingDown
=
mediaQuery
.
padding
.
top
+
_kToolbarScreenPadding
+
_kToolbarHeight
+
_kToolbarContentDistance
<=
globalEditableRegion
.
top
+
endpoints
.
first
.
point
.
dy
-
textLineHeight
;
final
double
arrowTipX
=
(
position
.
dx
+
globalEditableRegion
.
left
).
clamp
(
_kArrowScreenPadding
+
mediaQuery
.
padding
.
left
,
mediaQuery
.
size
.
width
-
mediaQuery
.
padding
.
right
-
_kArrowScreenPadding
,
);
// The toolbar should appear below the TextField
// when there is not enough space above the TextField to show it.
final
double
availableHeight
=
globalEditableRegion
.
top
-
MediaQuery
.
of
(
context
).
padding
.
top
-
_kToolbarScreenPadding
;
final
_ArrowDirection
direction
=
(
availableHeight
>
_kToolbarHeight
)
?
_ArrowDirection
.
down
:
_ArrowDirection
.
up
;
final
TextSelectionPoint
startTextSelectionPoint
=
endpoints
[
0
];
final
TextSelectionPoint
endTextSelectionPoint
=
(
endpoints
.
length
>
1
)
?
endpoints
[
1
]
:
null
;
final
double
x
=
(
endTextSelectionPoint
==
null
)
?
startTextSelectionPoint
.
point
.
dx
:
(
startTextSelectionPoint
.
point
.
dx
+
endTextSelectionPoint
.
point
.
dx
)
/
2.0
;
final
double
y
=
(
direction
==
_ArrowDirection
.
up
)
?
startTextSelectionPoint
.
point
.
dy
+
globalEditableRegion
.
height
+
_kToolbarHeight
:
startTextSelectionPoint
.
point
.
dy
-
globalEditableRegion
.
height
;
final
Offset
preciseMidpoint
=
Offset
(
x
,
y
);
return
ConstrainedBox
(
constraints:
BoxConstraints
.
tight
(
globalEditableRegion
.
size
),
child:
CustomSingleChildLayout
(
delegate:
_TextSelectionToolbarLayout
(
MediaQuery
.
of
(
context
).
size
,
globalEditableRegion
,
preciseMidpoint
,
),
child:
_TextSelectionToolbar
(
handleCut:
canCut
(
delegate
)
?
()
=>
handleCut
(
delegate
)
:
null
,
handleCopy:
canCopy
(
delegate
)
?
()
=>
handleCopy
(
delegate
)
:
null
,
handlePaste:
canPaste
(
delegate
)
?
()
=>
handlePaste
(
delegate
)
:
null
,
handleSelectAll:
canSelectAll
(
delegate
)
?
()
=>
handleSelectAll
(
delegate
)
:
null
,
arrowDirection:
direction
,
),
// The y-coordinate has to be calculated instead of directly quoting postion.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
;
final
List
<
Widget
>
items
=
<
Widget
>[];
final
Widget
onePhysicalPixelVerticalDivider
=
SizedBox
(
width:
1.0
/
MediaQuery
.
of
(
context
).
devicePixelRatio
);
final
CupertinoLocalizations
localizations
=
CupertinoLocalizations
.
of
(
context
);
final
EdgeInsets
arrowPadding
=
isArrowPointingDown
?
EdgeInsets
.
only
(
bottom:
_kToolbarArrowSize
.
height
)
:
EdgeInsets
.
only
(
top:
_kToolbarArrowSize
.
height
);
void
addToolbarButtonIfNeeded
(
String
text
,
bool
Function
(
TextSelectionDelegate
)
predicate
,
void
Function
(
TextSelectionDelegate
)
onPressed
)
{
if
(!
predicate
(
delegate
))
{
return
;
}
if
(
items
.
isNotEmpty
)
{
items
.
add
(
onePhysicalPixelVerticalDivider
);
}
items
.
add
(
CupertinoButton
(
child:
Text
(
text
,
style:
_kToolbarButtonFontStyle
),
color:
_kToolbarBackgroundColor
,
minSize:
_kToolbarHeight
,
padding:
_kToolbarButtonPadding
.
add
(
arrowPadding
),
borderRadius:
null
,
pressedOpacity:
0.7
,
onPressed:
()
=>
onPressed
(
delegate
),
));
}
addToolbarButtonIfNeeded
(
localizations
.
cutButtonLabel
,
canCut
,
handleCut
);
addToolbarButtonIfNeeded
(
localizations
.
copyButtonLabel
,
canCopy
,
handleCopy
);
addToolbarButtonIfNeeded
(
localizations
.
pasteButtonLabel
,
canPaste
,
handlePaste
);
addToolbarButtonIfNeeded
(
localizations
.
selectAllButtonLabel
,
canSelectAll
,
handleSelectAll
);
return
CupertinoTextSelectionToolbar
.
_
(
barTopY:
localBarTopY
+
globalEditableRegion
.
top
,
arrowTipX:
arrowTipX
,
isArrowPointingDown:
isArrowPointingDown
,
child:
items
.
isEmpty
?
null
:
DecoratedBox
(
decoration:
const
BoxDecoration
(
color:
_kToolbarDividerColor
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
items
),
),
);
}
...
...
packages/flutter/lib/src/material/text_selection.dart
View file @
26021191
...
...
@@ -141,6 +141,7 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
26021191
...
...
@@ -108,12 +108,16 @@ abstract class TextSelectionControls {
/// [globalEditableRegion] is the TextField size of the global coordinate system
/// in logical pixels.
///
/// [textLineHeight] is the `preferredLineHeight` of the [RenderEditable] we
/// are building a toolbar for.
///
/// The [position] is a general calculation midpoint parameter of the toolbar.
/// If you want more detailed position information, can use [endpoints]
/// to calculate it.
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
double
textLineHeight
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
...
...
@@ -509,19 +513,29 @@ class TextSelectionOverlay {
return
Container
();
// Find the horizontal midpoint, just above the selected text.
final
List
<
TextSelectionPoint
>
endpoints
=
renderObject
.
getEndpointsForSelection
(
_selection
);
final
Offset
midpoint
=
Offset
(
(
endpoints
.
length
==
1
)
?
endpoints
[
0
].
point
.
dx
:
(
endpoints
[
0
].
point
.
dx
+
endpoints
[
1
].
point
.
dx
)
/
2.0
,
endpoints
[
0
].
point
.
dy
-
renderObject
.
preferredLineHeight
,
);
final
List
<
TextSelectionPoint
>
endpoints
=
renderObject
.
getEndpointsForSelection
(
_selection
);
final
Rect
editingRegion
=
Rect
.
fromPoints
(
renderObject
.
localToGlobal
(
Offset
.
zero
),
renderObject
.
localToGlobal
(
renderObject
.
size
.
bottomRight
(
Offset
.
zero
)),
);
final
bool
isMultiline
=
endpoints
.
last
.
point
.
dy
-
endpoints
.
first
.
point
.
dy
>
renderObject
.
preferredLineHeight
/
2
;
// If the selected text spans more than 1 line, horizontally center the toolbar.
// Derived from both iOS and Android.
final
double
midX
=
isMultiline
?
editingRegion
.
width
/
2
:
(
endpoints
.
first
.
point
.
dx
+
endpoints
.
last
.
point
.
dx
)
/
2
;
final
Offset
midpoint
=
Offset
(
midX
,
// The y-coordinate won't be made use of most likely.
endpoints
[
0
].
point
.
dy
-
renderObject
.
preferredLineHeight
,
);
return
FadeTransition
(
opacity:
_toolbarOpacity
,
child:
CompositedTransformFollower
(
...
...
@@ -531,6 +545,7 @@ class TextSelectionOverlay {
child:
selectionControls
.
buildToolbar
(
context
,
editingRegion
,
renderObject
.
preferredLineHeight
,
midpoint
,
endpoints
,
selectionDelegate
,
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
26021191
...
...
@@ -11,6 +11,8 @@ import 'package:flutter/foundation.dart';
import
'package:flutter/gestures.dart'
show
DragStartBehavior
,
PointerDeviceKind
;
import
'package:flutter_test/flutter_test.dart'
;
import
'../rendering/mock_canvas.dart'
;
class
MockClipboard
{
Object
_clipboardData
=
<
String
,
dynamic
>{
'text'
:
null
,
...
...
@@ -27,6 +29,101 @@ class MockClipboard {
}
}
class
PathBoundsMatcher
extends
Matcher
{
const
PathBoundsMatcher
({
this
.
rectMatcher
,
this
.
topMatcher
,
this
.
leftMatcher
,
this
.
rightMatcher
,
this
.
bottomMatcher
,
})
:
super
();
final
Matcher
rectMatcher
;
final
Matcher
topMatcher
;
final
Matcher
leftMatcher
;
final
Matcher
rightMatcher
;
final
Matcher
bottomMatcher
;
@override
bool
matches
(
covariant
Path
item
,
Map
<
dynamic
,
dynamic
>
matchState
)
{
final
Rect
bounds
=
item
.
getBounds
();
final
List
<
Matcher
>
matchers
=
<
Matcher
>
[
rectMatcher
,
topMatcher
,
leftMatcher
,
rightMatcher
,
bottomMatcher
];
final
List
<
dynamic
>
values
=
<
dynamic
>
[
bounds
,
bounds
.
top
,
bounds
.
left
,
bounds
.
right
,
bounds
.
bottom
];
final
Map
<
Matcher
,
dynamic
>
failedMatcher
=
<
Matcher
,
dynamic
>
{};
for
(
int
idx
=
0
;
idx
<
matchers
.
length
;
idx
++)
{
if
(!(
matchers
[
idx
]?.
matches
(
values
[
idx
],
matchState
)
!=
false
))
{
failedMatcher
[
matchers
[
idx
]]
=
values
[
idx
];
}
}
matchState
[
'failedMatcher'
]
=
failedMatcher
;
return
failedMatcher
.
isEmpty
;
}
@override
Description
describe
(
Description
description
)
=>
description
.
add
(
'The actual Rect does not match'
);
@override
Description
describeMismatch
(
covariant
Path
item
,
Description
mismatchDescription
,
Map
<
dynamic
,
dynamic
>
matchState
,
bool
verbose
)
{
final
Description
description
=
super
.
describeMismatch
(
item
,
mismatchDescription
,
matchState
,
verbose
);
final
Map
<
Matcher
,
dynamic
>
map
=
matchState
[
'failedMatcher'
];
final
Iterable
<
String
>
descriptions
=
map
.
entries
.
map
<
String
>(
(
MapEntry
<
Matcher
,
dynamic
>
entry
)
=>
entry
.
key
.
describeMismatch
(
entry
.
value
,
StringDescription
(),
matchState
,
verbose
).
toString
()
);
// description is guaranteed to be non-null.
return
description
..
add
(
'mismatch Rect:
${item.getBounds()}
'
)
.
addAll
(
': '
,
', '
,
'. '
,
descriptions
);
}
}
class
PathPointsMatcher
extends
Matcher
{
const
PathPointsMatcher
({
this
.
includes
=
const
<
Offset
>[],
this
.
excludes
=
const
<
Offset
>[],
})
:
super
();
final
Iterable
<
Offset
>
includes
;
final
Iterable
<
Offset
>
excludes
;
@override
bool
matches
(
covariant
Path
item
,
Map
<
dynamic
,
dynamic
>
matchState
)
{
final
Offset
notIncluded
=
includes
.
firstWhere
((
Offset
offset
)
=>
!
item
.
contains
(
offset
),
orElse:
()
=>
null
);
final
Offset
notExcluded
=
excludes
.
firstWhere
(
item
.
contains
,
orElse:
()
=>
null
);
matchState
[
'notIncluded'
]
=
notIncluded
;
matchState
[
'notExcluded'
]
=
notExcluded
;
return
(
notIncluded
??
notExcluded
)
==
null
;
}
@override
Description
describe
(
Description
description
)
=>
description
.
add
(
'must include these points
$includes
and must not include
$excludes
'
);
@override
Description
describeMismatch
(
covariant
Path
item
,
Description
mismatchDescription
,
Map
<
dynamic
,
dynamic
>
matchState
,
bool
verbose
)
{
final
Offset
notIncluded
=
matchState
[
'notIncluded'
];
final
Offset
notExcluded
=
matchState
[
'notExcluded'
];
final
Description
desc
=
super
.
describeMismatch
(
item
,
mismatchDescription
,
matchState
,
verbose
);
if
((
notExcluded
??
notIncluded
)
!=
null
)
{
desc
.
add
(
'Within the bounds of the path
${item.getBounds()}
: '
);
}
if
(
notIncluded
!=
null
)
{
desc
.
add
(
'
$notIncluded
is not included. '
);
}
if
(
notExcluded
!=
null
)
{
desc
.
add
(
'
$notExcluded
is not excluded. '
);
}
return
desc
;
}
}
void
main
(
)
{
final
MockClipboard
mockClipboard
=
MockClipboard
();
SystemChannels
.
platform
.
setMockMethodCallHandler
(
mockClipboard
.
handleMethodCall
);
...
...
@@ -58,7 +155,7 @@ void main() {
}).
toList
();
}
Offset
textOffsetToPosition
(
WidgetTester
tester
,
int
offset
)
{
Offset
textOffsetTo
BottomLeft
Position
(
WidgetTester
tester
,
int
offset
)
{
final
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
final
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
...
...
@@ -67,9 +164,16 @@ void main() {
renderEditable
,
);
expect
(
endpoints
.
length
,
1
);
return
endpoints
[
0
].
point
+
const
Offset
(
0.0
,
-
2.0
)
;
return
endpoints
[
0
].
point
;
}
Offset
textOffsetToPosition
(
WidgetTester
tester
,
int
offset
)
=>
textOffsetToBottomLeftPosition
(
tester
,
offset
)
+
const
Offset
(
0
,
-
2
);
setUp
(()
{
EditableText
.
debugDeterministicCursor
=
false
;
});
testWidgets
(
'takes available space horizontally and takes intrinsic space vertically no-strut'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -1121,8 +1225,8 @@ void main() {
Text
text
=
tester
.
widget
<
Text
>(
find
.
text
(
'Paste'
));
expect
(
text
.
style
.
color
,
CupertinoColors
.
white
);
expect
(
text
.
style
.
fontSize
,
14
);
expect
(
text
.
style
.
letterSpacing
,
-
0.1
1
);
expect
(
text
.
style
.
fontWeight
,
FontWeight
.
w
3
00
);
expect
(
text
.
style
.
letterSpacing
,
-
0.1
5
);
expect
(
text
.
style
.
fontWeight
,
FontWeight
.
w
4
00
);
// Change the theme.
await
tester
.
pumpWidget
(
...
...
@@ -1153,8 +1257,8 @@ void main() {
// The toolbar buttons' text are still the same style.
expect
(
text
.
style
.
color
,
CupertinoColors
.
white
);
expect
(
text
.
style
.
fontSize
,
14
);
expect
(
text
.
style
.
letterSpacing
,
-
0.1
1
);
expect
(
text
.
style
.
fontWeight
,
FontWeight
.
w
3
00
);
expect
(
text
.
style
.
letterSpacing
,
-
0.1
5
);
expect
(
text
.
style
.
fontWeight
,
FontWeight
.
w
4
00
);
});
testWidgets
(
'Read only text field'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -2688,4 +2792,332 @@ void main() {
skip:
!
isLinux
,
);
});
group
(
'Text selection toolbar'
,
()
{
testWidgets
(
'Collapsed selection works'
,
(
WidgetTester
tester
)
async
{
EditableText
.
debugDeterministicCursor
=
true
;
tester
.
binding
.
window
.
physicalSizeTestValue
=
const
Size
(
400
,
400
);
tester
.
binding
.
window
.
devicePixelRatioTestValue
=
1
;
TextEditingController
controller
;
EditableTextState
state
;
Offset
bottomLeftSelectionPosition
;
controller
=
TextEditingController
(
text:
'a'
);
// Top left collapsed selection. The toolbar should flip vertically, and
// the arrow should not point exactly to the caret because the caret is
// too close to the left.
await
tester
.
pumpWidget
(
CupertinoApp
(
debugShowCheckedModeBanner:
false
,
home:
CupertinoPageScaffold
(
child:
Align
(
alignment:
Alignment
.
topLeft
,
child:
SizedBox
(
width:
200
,
height:
200
,
child:
CupertinoTextField
(
controller:
controller
,
maxLines:
null
,
),
),
),
),
),
);
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
final
double
lineHeight
=
state
.
renderEditable
.
preferredLineHeight
;
state
.
renderEditable
.
selectPositionAt
(
from:
textOffsetToPosition
(
tester
,
0
),
cause:
SelectionChangedCause
.
tap
);
expect
(
state
.
showToolbar
(),
true
);
await
tester
.
pumpAndSettle
();
bottomLeftSelectionPosition
=
textOffsetToBottomLeftPosition
(
tester
,
0
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathPointsMatcher
(
excludes:
<
Offset
>
[
// Arrow should not point to the selection handle.
bottomLeftSelectionPosition
.
translate
(
0
,
8
+
0.1
),
],
includes:
<
Offset
>
[
// Expected center of the arrow.
Offset
(
26.0
,
bottomLeftSelectionPosition
.
dy
+
8
+
0.1
),
],
),
),
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathBoundsMatcher
(
topMatcher:
moreOrLessEquals
(
bottomLeftSelectionPosition
.
dy
+
8
,
epsilon:
0.01
),
leftMatcher:
moreOrLessEquals
(
8
),
rightMatcher:
lessThanOrEqualTo
(
400
-
8
),
bottomMatcher:
moreOrLessEquals
(
bottomLeftSelectionPosition
.
dy
+
8
+
43
,
epsilon:
0.01
),
),
),
);
// Top Right collapsed selection. The toolbar should flip vertically, and
// the arrow should not point exactly to the caret because the caret is
// too close to the right.
controller
=
TextEditingController
(
text:
List
<
String
>.
filled
(
200
,
'a'
).
join
());
await
tester
.
pumpWidget
(
CupertinoApp
(
debugShowCheckedModeBanner:
false
,
home:
CupertinoPageScaffold
(
child:
Align
(
alignment:
Alignment
.
topRight
,
child:
SizedBox
(
width:
200
,
height:
200
,
child:
CupertinoTextField
(
controller:
controller
,
maxLines:
null
,
),
),
),
),
),
);
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
state
.
renderEditable
.
selectPositionAt
(
from:
tester
.
getTopRight
(
find
.
byType
(
CupertinoApp
)),
cause:
SelectionChangedCause
.
tap
);
expect
(
state
.
showToolbar
(),
true
);
await
tester
.
pumpAndSettle
();
// -1 because we want to reach the end of the line, not the start of a new line.
bottomLeftSelectionPosition
=
textOffsetToBottomLeftPosition
(
tester
,
state
.
renderEditable
.
selection
.
baseOffset
-
1
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathPointsMatcher
(
excludes:
<
Offset
>
[
// Arrow should not point to the selection handle.
bottomLeftSelectionPosition
.
translate
(
0
,
8
+
0.1
),
],
includes:
<
Offset
>
[
// Expected center of the arrow.
Offset
(
400
-
26.0
,
bottomLeftSelectionPosition
.
dy
+
8
+
0.1
),
],
),
),
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathBoundsMatcher
(
topMatcher:
moreOrLessEquals
(
bottomLeftSelectionPosition
.
dy
+
8
,
epsilon:
0.01
),
rightMatcher:
moreOrLessEquals
(
400.0
-
8
),
bottomMatcher:
moreOrLessEquals
(
bottomLeftSelectionPosition
.
dy
+
8
+
43
,
epsilon:
0.01
),
leftMatcher:
greaterThanOrEqualTo
(
8
),
),
),
);
// Normal centered collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the caret.
controller
=
TextEditingController
(
text:
List
<
String
>.
filled
(
200
,
'a'
).
join
());
await
tester
.
pumpWidget
(
CupertinoApp
(
debugShowCheckedModeBanner:
false
,
home:
CupertinoPageScaffold
(
child:
Align
(
alignment:
Alignment
.
center
,
child:
SizedBox
(
width:
200
,
height:
200
,
child:
CupertinoTextField
(
controller:
controller
,
maxLines:
null
,
),
),
),
),
),
);
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
state
.
renderEditable
.
selectPositionAt
(
from:
tester
.
getCenter
(
find
.
byType
(
EditableText
)),
cause:
SelectionChangedCause
.
tap
);
expect
(
state
.
showToolbar
(),
true
);
await
tester
.
pumpAndSettle
();
bottomLeftSelectionPosition
=
textOffsetToBottomLeftPosition
(
tester
,
state
.
renderEditable
.
selection
.
baseOffset
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathPointsMatcher
(
includes:
<
Offset
>
[
// Expected center of the arrow.
bottomLeftSelectionPosition
.
translate
(
0
,
-
lineHeight
-
8
-
0.1
),
],
),
),
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathBoundsMatcher
(
bottomMatcher:
moreOrLessEquals
(
bottomLeftSelectionPosition
.
dy
-
8
-
lineHeight
,
epsilon:
0.01
),
topMatcher:
moreOrLessEquals
(
bottomLeftSelectionPosition
.
dy
-
8
-
lineHeight
-
43
,
epsilon:
0.01
),
rightMatcher:
lessThanOrEqualTo
(
400
-
8
),
leftMatcher:
greaterThanOrEqualTo
(
8
),
),
),
);
});
testWidgets
(
'selecting multiple words works'
,
(
WidgetTester
tester
)
async
{
EditableText
.
debugDeterministicCursor
=
true
;
tester
.
binding
.
window
.
physicalSizeTestValue
=
const
Size
(
400
,
400
);
tester
.
binding
.
window
.
devicePixelRatioTestValue
=
1
;
TextEditingController
controller
;
EditableTextState
state
;
// Normal multiword collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the caret.
controller
=
TextEditingController
(
text:
List
<
String
>.
filled
(
20
,
'a'
).
join
(
' '
));
await
tester
.
pumpWidget
(
CupertinoApp
(
debugShowCheckedModeBanner:
false
,
home:
CupertinoPageScaffold
(
child:
Align
(
alignment:
Alignment
.
center
,
child:
SizedBox
(
width:
200
,
height:
200
,
child:
CupertinoTextField
(
controller:
controller
,
maxLines:
null
,
),
),
),
),
),
);
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
final
double
lineHeight
=
state
.
renderEditable
.
preferredLineHeight
;
// Select the first 2 words.
state
.
renderEditable
.
selectPositionAt
(
from:
textOffsetToPosition
(
tester
,
0
),
to:
textOffsetToPosition
(
tester
,
4
),
cause:
SelectionChangedCause
.
tap
);
expect
(
state
.
showToolbar
(),
true
);
await
tester
.
pumpAndSettle
();
final
Offset
selectionPosition
=
(
textOffsetToBottomLeftPosition
(
tester
,
0
)
+
textOffsetToBottomLeftPosition
(
tester
,
4
))
/
2
;
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathPointsMatcher
(
includes:
<
Offset
>
[
// Expected center of the arrow.
selectionPosition
.
translate
(
0
,
-
lineHeight
-
8
-
0.1
),
],
),
),
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathBoundsMatcher
(
bottomMatcher:
moreOrLessEquals
(
selectionPosition
.
dy
-
8
-
lineHeight
,
epsilon:
0.01
),
topMatcher:
moreOrLessEquals
(
selectionPosition
.
dy
-
8
-
lineHeight
-
43
,
epsilon:
0.01
),
rightMatcher:
lessThanOrEqualTo
(
400
-
8
),
leftMatcher:
greaterThanOrEqualTo
(
8
),
),
),
);
});
testWidgets
(
'selecting multiline works'
,
(
WidgetTester
tester
)
async
{
EditableText
.
debugDeterministicCursor
=
true
;
tester
.
binding
.
window
.
physicalSizeTestValue
=
const
Size
(
400
,
400
);
tester
.
binding
.
window
.
devicePixelRatioTestValue
=
1
;
TextEditingController
controller
;
EditableTextState
state
;
// Normal multiline collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the horizontal center of the text field.
controller
=
TextEditingController
(
text:
List
<
String
>.
filled
(
20
,
'a a '
).
join
(
'
\n
'
));
await
tester
.
pumpWidget
(
CupertinoApp
(
debugShowCheckedModeBanner:
false
,
home:
CupertinoPageScaffold
(
child:
Align
(
alignment:
Alignment
.
center
,
child:
SizedBox
(
width:
200
,
height:
200
,
child:
CupertinoTextField
(
controller:
controller
,
maxLines:
null
,
),
),
),
),
),
);
state
=
tester
.
state
<
EditableTextState
>(
find
.
byType
(
EditableText
));
final
double
lineHeight
=
state
.
renderEditable
.
preferredLineHeight
;
// Select the first 2 words.
state
.
renderEditable
.
selectPositionAt
(
from:
textOffsetToPosition
(
tester
,
0
),
to:
textOffsetToPosition
(
tester
,
10
),
cause:
SelectionChangedCause
.
tap
);
expect
(
state
.
showToolbar
(),
true
);
await
tester
.
pumpAndSettle
();
final
Offset
selectionPosition
=
Offset
(
// Toolbar should be centered.
200
,
textOffsetToBottomLeftPosition
(
tester
,
0
).
dy
,
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathPointsMatcher
(
includes:
<
Offset
>
[
// Expected center of the arrow.
selectionPosition
.
translate
(
0
,
-
lineHeight
-
8
-
0.1
),
],
),
),
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paints
..
clipPath
(
pathMatcher:
PathBoundsMatcher
(
bottomMatcher:
moreOrLessEquals
(
selectionPosition
.
dy
-
8
-
lineHeight
,
epsilon:
0.01
),
topMatcher:
moreOrLessEquals
(
selectionPosition
.
dy
-
8
-
lineHeight
-
43
,
epsilon:
0.01
),
rightMatcher:
lessThanOrEqualTo
(
400
-
8
),
leftMatcher:
greaterThanOrEqualTo
(
8
),
),
),
);
});
});
}
packages/flutter/test/material/text_field_test.dart
View file @
26021191
...
...
@@ -15,6 +15,7 @@ import 'package:flutter/services.dart';
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
,
PointerDeviceKind
;
import
'../rendering/mock_canvas.dart'
;
import
'../widgets/semantics_tester.dart'
;
import
'feedback_tester.dart'
;
...
...
@@ -864,7 +865,7 @@ void main() {
expect
(
find
.
text
(
'CUT'
),
findsNothing
);
});
testWidgets
(
'
text field build empty tool bar when no options available
ios'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'
does not paint tool bar when no options available on
ios'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
...
...
@@ -882,11 +883,8 @@ void main() {
await
tester
.
tap
(
find
.
byType
(
TextField
));
// Wait for context menu to be built.
await
tester
.
pumpAndSettle
();
final
RenderBox
container
=
tester
.
renderObject
(
find
.
descendant
(
of:
find
.
byType
(
FadeTransition
),
matching:
find
.
byType
(
Container
),
));
expect
(
container
.
size
,
Size
.
zero
);
expect
(
find
.
byType
(
CupertinoTextSelectionToolbar
),
paintsNothing
);
});
testWidgets
(
'text field build empty tool bar when no options available android'
,
(
WidgetTester
tester
)
async
{
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
26021191
...
...
@@ -1546,7 +1546,7 @@ void main() {
controls
=
MockTextSelectionControls
();
when
(
controls
.
buildHandle
(
any
,
any
,
any
)).
thenReturn
(
Container
());
when
(
controls
.
buildToolbar
(
any
,
any
,
any
,
any
,
any
))
when
(
controls
.
buildToolbar
(
any
,
any
,
any
,
any
,
any
,
any
))
.
thenReturn
(
Container
());
});
...
...
packages/flutter_test/lib/src/matchers.dart
View file @
26021191
...
...
@@ -1108,7 +1108,8 @@ class _IsWithinDistance<T> extends Matcher {
}
class
_MoreOrLessEquals
extends
Matcher
{
const
_MoreOrLessEquals
(
this
.
value
,
this
.
epsilon
);
const
_MoreOrLessEquals
(
this
.
value
,
this
.
epsilon
)
:
assert
(
epsilon
>=
0
);
final
double
value
;
final
double
epsilon
;
...
...
@@ -1125,6 +1126,12 @@ class _MoreOrLessEquals extends Matcher {
@override
Description
describe
(
Description
description
)
=>
description
.
add
(
'
$value
(±
$epsilon
)'
);
@override
Description
describeMismatch
(
Object
item
,
Description
mismatchDescription
,
Map
<
dynamic
,
dynamic
>
matchState
,
bool
verbose
)
{
return
super
.
describeMismatch
(
item
,
mismatchDescription
,
matchState
,
verbose
)
..
add
(
'
$item
is not in the range of
$value
(±
$epsilon
).'
);
}
}
class
_IsMethodCall
extends
Matcher
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment