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
c54ce015
Unverified
Commit
c54ce015
authored
Jan 27, 2022
by
Jami Couch
Committed by
GitHub
Jan 27, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Re-land "Support Scribble Handwriting" (#96615) (#96881)
parent
86355950
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
1248 additions
and
53 deletions
+1248
-53
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+10
-0
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+7
-1
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+10
-0
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+157
-1
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+328
-49
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+2
-2
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+32
-0
autofill_test.dart
packages/flutter/test/services/autofill_test.dart
+17
-0
delta_text_input_test.dart
packages/flutter/test/services/delta_text_input_test.dart
+16
-0
text_input_test.dart
packages/flutter/test/services/text_input_test.dart
+158
-0
text_input_utils.dart
packages/flutter/test/services/text_input_utils.dart
+27
-0
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+402
-0
test_text_input.dart
packages/flutter_test/lib/src/test_text_input.dart
+82
-0
No files found.
packages/flutter/lib/src/cupertino/text_field.dart
View file @
c54ce015
...
...
@@ -297,6 +297,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
autofillHints
=
const
<
String
>[],
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
...
...
@@ -468,6 +469,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
autofillHints
=
const
<
String
>[],
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
...
...
@@ -826,6 +828,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.material.textfield.restorationId}
final
String
?
restorationId
;
/// {@macro flutter.widgets.editableText.scribbleEnabled}
final
bool
scribbleEnabled
;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final
bool
enableIMEPersonalizedLearning
;
...
...
@@ -871,6 +876,7 @@ class CupertinoTextField extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
TextAlignVertical
>(
'textAlignVertical'
,
textAlignVertical
,
defaultValue:
null
));
properties
.
add
(
EnumProperty
<
TextDirection
>(
'textDirection'
,
textDirection
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Clip
>(
'clipBehavior'
,
clipBehavior
,
defaultValue:
Clip
.
hardEdge
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
}
}
...
...
@@ -991,6 +997,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
if
(
cause
==
SelectionChangedCause
.
keyboard
)
return
false
;
if
(
cause
==
SelectionChangedCause
.
scribble
)
return
true
;
if
(
_effectiveController
.
text
.
isNotEmpty
)
return
true
;
...
...
@@ -1320,6 +1329,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
autofillClient:
this
,
clipBehavior:
widget
.
clipBehavior
,
restorationId:
'editable'
,
scribbleEnabled:
widget
.
scribbleEnabled
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
),
),
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
c54ce015
...
...
@@ -329,6 +329,7 @@ class TextField extends StatefulWidget {
this
.
autofillHints
=
const
<
String
>[],
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
...
...
@@ -781,6 +782,9 @@ class TextField extends StatefulWidget {
/// {@endtemplate}
final
String
?
restorationId
;
/// {@macro flutter.widgets.editableText.scribbleEnabled}
final
bool
scribbleEnabled
;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final
bool
enableIMEPersonalizedLearning
;
...
...
@@ -825,6 +829,7 @@ class TextField extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
ScrollController
>(
'scrollController'
,
scrollController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Clip
>(
'clipBehavior'
,
clipBehavior
,
defaultValue:
Clip
.
hardEdge
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
}
}
...
...
@@ -1042,7 +1047,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
if
(!
_isEnabled
)
return
false
;
if
(
cause
==
SelectionChangedCause
.
longPress
)
if
(
cause
==
SelectionChangedCause
.
longPress
||
cause
==
SelectionChangedCause
.
scribble
)
return
true
;
if
(
_effectiveController
.
text
.
isNotEmpty
)
...
...
@@ -1286,6 +1291,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
autocorrectionTextRectColor:
autocorrectionTextRectColor
,
clipBehavior:
widget
.
clipBehavior
,
restorationId:
'editable'
,
scribbleEnabled:
widget
.
scribbleEnabled
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
),
),
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
c54ce015
...
...
@@ -1265,6 +1265,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// [assembleSemanticsNode] invocations.
Queue
<
SemanticsNode
>?
_cachedChildNodes
;
/// Returns a list of rects that bound the given selection.
///
/// See [TextPainter.getBoxesForSelection] for more details.
List
<
Rect
>
getBoxesForSelection
(
TextSelection
selection
)
{
_computeTextMetricsIfNeeded
();
return
_textPainter
.
getBoxesForSelection
(
selection
)
.
map
((
TextBox
textBox
)
=>
textBox
.
toRect
().
shift
(
_paintOffset
))
.
toList
();
}
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
...
...
packages/flutter/lib/src/services/text_input.dart
View file @
c54ce015
...
...
@@ -955,6 +955,9 @@ enum SelectionChangedCause {
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag
,
/// The user used iPadOS 14+ Scribble to change the selection.
scribble
,
}
/// A mixin for manipulating the selection, provided for toolbar or shortcut
...
...
@@ -1105,6 +1108,76 @@ abstract class TextInputClient {
///
/// [TextInputClient] should cleanup its connection and finalize editing.
void
connectionClosed
();
/// Requests that the client show the editing toolbar, for example when the
/// platform changes the selection through a non-flutter method such as
/// scribble.
void
showToolbar
()
{}
/// Requests that the client add a text placeholder to reserve visual space
/// in the text.
///
/// For example, this is called when responding to UIKit requesting
/// a text placeholder be added at the current selection, such as when
/// requesting additional writing space with iPadOS14 Scribble.
void
insertTextPlaceholder
(
Size
size
)
{}
/// Requests that the client remove the text placeholder.
void
removeTextPlaceholder
()
{}
}
/// An interface to receive focus from the engine.
///
/// This is currently only used to handle UIIndirectScribbleInteraction.
abstract
class
ScribbleClient
{
/// A unique identifier for this element.
String
get
elementIdentifier
;
/// Called by the engine when the [ScribbleClient] should receive focus.
///
/// For example, this method is called during a UIIndirectScribbleInteraction.
void
onScribbleFocus
(
Offset
offset
);
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds.
bool
isInScribbleRect
(
Rect
rect
);
/// The current bounds of the [ScribbleClient].
Rect
get
bounds
;
}
/// Represents a selection rect for a character and it's position in the text.
///
/// This is used to report the current text selection rect and position data
/// to the engine for Scribble support on iPadOS 14.
@immutable
class
SelectionRect
{
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
const
SelectionRect
({
required
this
.
position
,
required
this
.
bounds
});
/// The position of this selection rect within the text String.
final
int
position
;
/// The rectangle representing the bounds of this selection rect within the
/// currently focused [RenderEditable]'s coordinate space.
final
Rect
bounds
;
@override
bool
operator
==(
Object
other
)
{
if
(
identical
(
this
,
other
))
return
true
;
if
(
runtimeType
!=
other
.
runtimeType
)
return
false
;
return
other
is
SelectionRect
&&
other
.
position
==
position
&&
other
.
bounds
==
bounds
;
}
@override
int
get
hashCode
=>
hashValues
(
position
,
bounds
);
@override
String
toString
()
=>
'SelectionRect(
$position
,
$bounds
)'
;
}
/// An interface to receive granular information from [TextInput].
...
...
@@ -1154,6 +1227,7 @@ class TextInputConnection {
Matrix4
?
_cachedTransform
;
Rect
?
_cachedRect
;
Rect
?
_cachedCaretRect
;
List
<
SelectionRect
>
_cachedSelectionRects
=
<
SelectionRect
>[];
static
int
_nextId
=
1
;
final
int
_id
;
...
...
@@ -1176,6 +1250,12 @@ class TextInputConnection {
/// Whether this connection is currently interacting with the text input control.
bool
get
attached
=>
TextInput
.
_instance
.
_currentConnection
==
this
;
/// Whether there is currently a Scribble interaction in progress.
///
/// This is used to make sure selection handles are shown when UIKit changes
/// the selection during a Scribble interaction.
bool
get
scribbleInProgress
=>
TextInput
.
_instance
.
scribbleInProgress
;
/// Requests that the text input control become visible.
void
show
()
{
assert
(
attached
);
...
...
@@ -1274,6 +1354,19 @@ class TextInputConnection {
);
}
/// Send the bounding boxes of the current selected glyphs in the client to
/// the platform's text input plugin.
///
/// These are used by the engine during a UIDirectScribbleInteraction.
void
setSelectionRects
(
List
<
SelectionRect
>
selectionRects
)
{
if
(!
listEquals
(
_cachedSelectionRects
,
selectionRects
))
{
_cachedSelectionRects
=
selectionRects
;
TextInput
.
_instance
.
_setSelectionRects
(
selectionRects
.
map
((
SelectionRect
rect
)
{
return
<
num
>[
rect
.
bounds
.
left
,
rect
.
bounds
.
top
,
rect
.
bounds
.
width
,
rect
.
bounds
.
height
,
rect
.
position
];
}).
toList
());
}
}
/// Send text styling information.
///
/// This information is used by the Flutter Web Engine to change the style
...
...
@@ -1535,10 +1628,43 @@ class TextInput {
TextInputConnection
?
_currentConnection
;
late
TextInputConfiguration
_currentConfiguration
;
final
Map
<
String
,
ScribbleClient
>
_scribbleClients
=
<
String
,
ScribbleClient
>{};
bool
_scribbleInProgress
=
false
;
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
@visibleForTesting
static
Map
<
String
,
ScribbleClient
>
get
scribbleClients
=>
TextInput
.
_instance
.
_scribbleClients
;
/// Returns true if a scribble interaction is currently happening.
bool
get
scribbleInProgress
=>
_scribbleInProgress
;
Future
<
dynamic
>
_handleTextInputInvocation
(
MethodCall
methodCall
)
async
{
final
String
method
=
methodCall
.
method
;
if
(
method
==
'TextInputClient.focusElement'
)
{
final
List
<
dynamic
>
args
=
methodCall
.
arguments
as
List
<
dynamic
>;
_scribbleClients
[
args
[
0
]]?.
onScribbleFocus
(
Offset
((
args
[
1
]
as
num
).
toDouble
(),
(
args
[
2
]
as
num
).
toDouble
()));
return
;
}
else
if
(
method
==
'TextInputClient.requestElementsInRect'
)
{
final
List
<
double
>
args
=
(
methodCall
.
arguments
as
List
<
dynamic
>).
cast
<
num
>().
map
<
double
>((
num
value
)
=>
value
.
toDouble
()).
toList
();
return
_scribbleClients
.
keys
.
where
((
String
elementIdentifier
)
{
final
Rect
rect
=
Rect
.
fromLTWH
(
args
[
0
],
args
[
1
],
args
[
2
],
args
[
3
]);
if
(!(
_scribbleClients
[
elementIdentifier
]?.
isInScribbleRect
(
rect
)
??
false
))
return
false
;
final
Rect
bounds
=
_scribbleClients
[
elementIdentifier
]?.
bounds
??
Rect
.
zero
;
return
!(
bounds
==
Rect
.
zero
||
bounds
.
hasNaN
||
bounds
.
isInfinite
);
}).
map
((
String
elementIdentifier
)
{
final
Rect
bounds
=
_scribbleClients
[
elementIdentifier
]!.
bounds
;
return
<
dynamic
>[
elementIdentifier
,
...<
dynamic
>[
bounds
.
left
,
bounds
.
top
,
bounds
.
width
,
bounds
.
height
]];
}).
toList
();
}
else
if
(
method
==
'TextInputClient.scribbleInteractionBegan'
)
{
_scribbleInProgress
=
true
;
return
;
}
else
if
(
method
==
'TextInputClient.scribbleInteractionFinished'
)
{
_scribbleInProgress
=
false
;
return
;
}
if
(
_currentConnection
==
null
)
return
;
final
String
method
=
methodCall
.
method
;
// The requestExistingInputState request needs to be handled regardless of
// the client ID, as long as we have a _currentConnection.
...
...
@@ -1630,6 +1756,15 @@ class TextInput {
case
'TextInputClient.showAutocorrectionPromptRect'
:
_currentConnection
!.
_client
.
showAutocorrectionPromptRect
(
args
[
1
]
as
int
,
args
[
2
]
as
int
);
break
;
case
'TextInputClient.showToolbar'
:
_currentConnection
!.
_client
.
showToolbar
();
break
;
case
'TextInputClient.insertTextPlaceholder'
:
_currentConnection
!.
_client
.
insertTextPlaceholder
(
Size
((
args
[
1
]
as
num
).
toDouble
(),
(
args
[
2
]
as
num
).
toDouble
()));
break
;
case
'TextInputClient.removeTextPlaceholder'
:
_currentConnection
!.
_client
.
removeTextPlaceholder
();
break
;
default
:
throw
MissingPluginException
();
}
...
...
@@ -1703,6 +1838,13 @@ class TextInput {
);
}
void
_setSelectionRects
(
List
<
List
<
num
>>
args
)
{
_channel
.
invokeMethod
<
void
>(
'TextInput.setSelectionRects'
,
args
,
);
}
void
_setStyle
(
Map
<
String
,
dynamic
>
args
)
{
_channel
.
invokeMethod
<
void
>(
'TextInput.setStyle'
,
...
...
@@ -1765,4 +1907,18 @@ class TextInput {
shouldSave
,
);
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
/// by the engine.
///
/// For example, the registered [ScribbleClient] list is used to respond to
/// UIIndirectScribbleInteraction on an iPad.
static
void
registerScribbleElement
(
String
elementIdentifier
,
ScribbleClient
scribbleClient
)
{
TextInput
.
_instance
.
_scribbleClients
[
elementIdentifier
]
=
scribbleClient
;
}
/// Unregisters a [ScribbleClient] with [elementIdentifier].
static
void
unregisterScribbleElement
(
String
elementIdentifier
)
{
TextInput
.
_instance
.
_scribbleClients
.
remove
(
elementIdentifier
);
}
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
c54ce015
...
...
@@ -6,7 +6,7 @@ import 'dart:async';
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
hide
TextStyle
;
import
'package:characters/characters.dart'
show
CharacterRange
;
import
'package:characters/characters.dart'
show
CharacterRange
,
StringCharacters
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
import
'package:flutter/rendering.dart'
;
...
...
@@ -58,6 +58,10 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// is shown in an obscured text field.
const
int
_kObscureShowLatestCharCursorTicks
=
3
;
// The minimum width of an iPad screen. The smallest iPad is currently the
// iPad Mini 6th Gen according to ios-resolution.com.
const
double
_kIPadWidth
=
1488.0
;
/// A controller for an editable text field.
///
/// Whenever the user modifies a text field with an associated
...
...
@@ -518,6 +522,7 @@ class EditableText extends StatefulWidget {
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
scrollBehavior
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
controller
!=
null
),
assert
(
focusNode
!=
null
),
...
...
@@ -1232,6 +1237,15 @@ class EditableText extends StatefulWidget {
/// [scrollPhysics].
final
ScrollPhysics
?
scrollPhysics
;
/// {@template flutter.widgets.editableText.scribbleEnabled}
/// Whether iOS 14 Scribble features are enabled for this widget.
///
/// Only available on iPads.
///
/// Defaults to true.
/// {@endtemplate}
final
bool
scribbleEnabled
;
/// {@template flutter.widgets.editableText.selectionEnabled}
/// Same as [enableInteractiveSelection].
///
...
...
@@ -1533,6 +1547,7 @@ class EditableText extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Iterable
<
String
>>(
'autofillHints'
,
autofillHints
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
TextHeightBehavior
>(
'textHeightBehavior'
,
textHeightBehavior
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'scribbleEnabled'
,
scribbleEnabled
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableInteractiveSelection'
,
enableInteractiveSelection
,
defaultValue:
true
));
}
...
...
@@ -1902,7 +1917,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
)
{
// `selection` is the only change.
_handleSelectionChanged
(
value
.
selection
,
SelectionChangedCause
.
keyboard
);
_handleSelectionChanged
(
value
.
selection
,
(
_textInputConnection
?.
scribbleInProgress
??
false
)
?
SelectionChangedCause
.
scribble
:
SelectionChangedCause
.
keyboard
);
}
else
{
hideToolbar
();
_currentPromptRectRange
=
null
;
...
...
@@ -2686,6 +2701,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
null
);
}
_cachedText
=
''
;
_cachedFirstRect
=
null
;
_cachedSize
=
Size
.
zero
;
_cachedPlaceholder
=
-
1
;
}
else
{
WidgetsBinding
.
instance
!.
removeObserver
(
this
);
setState
(()
{
_currentPromptRectRange
=
null
;
});
...
...
@@ -2693,13 +2713,78 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
updateKeepAlive
();
}
String
_cachedText
=
''
;
Rect
?
_cachedFirstRect
;
Size
_cachedSize
=
Size
.
zero
;
int
_cachedPlaceholder
=
-
1
;
TextStyle
?
_cachedTextStyle
;
void
_updateSelectionRects
({
bool
force
=
false
})
{
if
(!
widget
.
scribbleEnabled
)
return
;
if
(
defaultTargetPlatform
!=
TargetPlatform
.
iOS
)
return
;
// This is to avoid sending selection rects on non-iPad devices.
if
(
WidgetsBinding
.
instance
!.
window
.
physicalSize
.
shortestSide
<
_kIPadWidth
)
return
;
final
String
text
=
renderEditable
.
text
?.
toPlainText
(
includeSemanticsLabels:
false
,
includePlaceholders:
false
)
??
''
;
final
List
<
Rect
>
firstSelectionBoxes
=
renderEditable
.
getBoxesForSelection
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
1
));
final
Rect
?
firstRect
=
firstSelectionBoxes
.
isNotEmpty
?
firstSelectionBoxes
.
first
:
null
;
final
ScrollDirection
scrollDirection
=
_scrollController
.
position
.
userScrollDirection
;
final
Size
size
=
renderEditable
.
size
;
final
bool
textChanged
=
text
!=
_cachedText
;
final
bool
textStyleChanged
=
_cachedTextStyle
!=
widget
.
style
;
final
bool
firstRectChanged
=
_cachedFirstRect
!=
firstRect
;
final
bool
sizeChanged
=
_cachedSize
!=
size
;
final
bool
placeholderChanged
=
_cachedPlaceholder
!=
_placeholderLocation
;
if
(
scrollDirection
==
ScrollDirection
.
idle
&&
(
force
||
textChanged
||
textStyleChanged
||
firstRectChanged
||
sizeChanged
||
placeholderChanged
))
{
_cachedText
=
text
;
_cachedFirstRect
=
firstRect
;
_cachedTextStyle
=
widget
.
style
;
_cachedSize
=
size
;
_cachedPlaceholder
=
_placeholderLocation
;
bool
belowRenderEditableBottom
=
false
;
final
List
<
SelectionRect
>
rects
=
List
<
SelectionRect
?>.
generate
(
_cachedText
.
characters
.
length
,
(
int
i
)
{
if
(
belowRenderEditableBottom
)
return
null
;
final
int
offset
=
_cachedText
.
characters
.
getRange
(
0
,
i
).
string
.
length
;
final
SelectionRect
selectionRect
=
SelectionRect
(
bounds:
renderEditable
.
getBoxesForSelection
(
TextSelection
(
baseOffset:
offset
,
extentOffset:
offset
+
_cachedText
.
characters
.
characterAt
(
i
).
string
.
length
)).
first
,
position:
offset
,
);
if
(
renderEditable
.
paintBounds
.
bottom
<
selectionRect
.
bounds
.
top
)
{
belowRenderEditableBottom
=
true
;
return
null
;
}
return
selectionRect
;
},
).
where
((
SelectionRect
?
selectionRect
)
{
if
(
selectionRect
==
null
)
return
false
;
if
(
renderEditable
.
paintBounds
.
right
<
selectionRect
.
bounds
.
left
||
selectionRect
.
bounds
.
right
<
renderEditable
.
paintBounds
.
left
)
return
false
;
if
(
renderEditable
.
paintBounds
.
bottom
<
selectionRect
.
bounds
.
top
||
selectionRect
.
bounds
.
bottom
<
renderEditable
.
paintBounds
.
top
)
return
false
;
return
true
;
}).
map
<
SelectionRect
>((
SelectionRect
?
selectionRect
)
=>
selectionRect
!).
toList
();
_textInputConnection
!.
setSelectionRects
(
rects
);
}
}
void
_updateSizeAndTransform
()
{
if
(
_hasInputConnection
)
{
final
Size
size
=
renderEditable
.
size
;
final
Matrix4
transform
=
renderEditable
.
getTransformTo
(
null
);
_textInputConnection
!.
setEditableSizeAndTransform
(
size
,
transform
);
_updateSelectionRects
();
SchedulerBinding
.
instance
!
.
addPostFrameCallback
((
Duration
_
)
=>
_updateSizeAndTransform
());
}
else
if
(
_placeholderLocation
!=
-
1
)
{
removeTextPlaceholder
();
}
}
...
...
@@ -2782,6 +2867,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
///
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// is already shown, or when no text selection currently exists.
@override
bool
showToolbar
()
{
// Web is using native dom elements to enable clipboard functionality of the
// toolbar: copy, paste, select, cut. It might also provide additional
...
...
@@ -2820,6 +2906,36 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
// text.
//
// A value of -1 indicates there should be no placeholder, otherwise the
// value should be between 0 and the length of the text, inclusive.
int
_placeholderLocation
=
-
1
;
@override
void
insertTextPlaceholder
(
Size
size
)
{
if
(!
widget
.
scribbleEnabled
)
return
;
if
(!
widget
.
controller
.
selection
.
isValid
)
return
;
setState
(()
{
_placeholderLocation
=
_value
.
text
.
length
-
widget
.
controller
.
selection
.
end
;
});
}
@override
void
removeTextPlaceholder
()
{
if
(!
widget
.
scribbleEnabled
)
return
;
setState
(()
{
_placeholderLocation
=
-
1
;
});
}
@override
String
get
autofillId
=>
'EditableText-
$hashCode
'
;
...
...
@@ -3046,53 +3162,62 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onCopy:
_semanticsOnCopy
(
controls
),
onCut:
_semanticsOnCut
(
controls
),
onPaste:
_semanticsOnPaste
(
controls
),
child:
_Editable
(
key:
_editableKey
,
startHandleLayerLink:
_startHandleLayerLink
,
endHandleLayerLink:
_endHandleLayerLink
,
inlineSpan:
buildTextSpan
(),
value:
_value
,
cursorColor:
_cursorColor
,
backgroundCursorColor:
widget
.
backgroundCursorColor
,
showCursor:
EditableText
.
debugDeterministicCursor
?
ValueNotifier
<
bool
>(
widget
.
showCursor
)
:
_cursorVisibilityNotifier
,
forceLine:
widget
.
forceLine
,
readOnly:
widget
.
readOnly
,
hasFocus:
_hasFocus
,
maxLines:
widget
.
maxLines
,
minLines:
widget
.
minLines
,
expands:
widget
.
expands
,
strutStyle:
widget
.
strutStyle
,
selectionColor:
widget
.
selectionColor
,
textScaleFactor:
widget
.
textScaleFactor
??
MediaQuery
.
textScaleFactorOf
(
context
),
textAlign:
widget
.
textAlign
,
textDirection:
_textDirection
,
locale:
widget
.
locale
,
textHeightBehavior:
widget
.
textHeightBehavior
??
DefaultTextHeightBehavior
.
of
(
context
),
textWidthBasis:
widget
.
textWidthBasis
,
obscuringCharacter:
widget
.
obscuringCharacter
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
smartDashesType:
widget
.
smartDashesType
,
smartQuotesType:
widget
.
smartQuotesType
,
enableSuggestions:
widget
.
enableSuggestions
,
offset:
offset
,
onCaretChanged:
_handleCaretChanged
,
rendererIgnoresPointer:
widget
.
rendererIgnoresPointer
,
cursorWidth:
widget
.
cursorWidth
,
cursorHeight:
widget
.
cursorHeight
,
cursorRadius:
widget
.
cursorRadius
,
cursorOffset:
widget
.
cursorOffset
??
Offset
.
zero
,
selectionHeightStyle:
widget
.
selectionHeightStyle
,
selectionWidthStyle:
widget
.
selectionWidthStyle
,
paintCursorAboveText:
widget
.
paintCursorAboveText
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
&&
(!
widget
.
readOnly
||
!
widget
.
obscureText
),
textSelectionDelegate:
this
,
devicePixelRatio:
_devicePixelRatio
,
promptRectRange:
_currentPromptRectRange
,
promptRectColor:
widget
.
autocorrectionTextRectColor
,
clipBehavior:
widget
.
clipBehavior
,
child:
_ScribbleFocusable
(
focusNode:
widget
.
focusNode
,
editableKey:
_editableKey
,
enabled:
widget
.
scribbleEnabled
,
updateSelectionRects:
()
{
_openInputConnection
();
_updateSelectionRects
(
force:
true
);
},
child:
_Editable
(
key:
_editableKey
,
startHandleLayerLink:
_startHandleLayerLink
,
endHandleLayerLink:
_endHandleLayerLink
,
inlineSpan:
buildTextSpan
(),
value:
_value
,
cursorColor:
_cursorColor
,
backgroundCursorColor:
widget
.
backgroundCursorColor
,
showCursor:
EditableText
.
debugDeterministicCursor
?
ValueNotifier
<
bool
>(
widget
.
showCursor
)
:
_cursorVisibilityNotifier
,
forceLine:
widget
.
forceLine
,
readOnly:
widget
.
readOnly
,
hasFocus:
_hasFocus
,
maxLines:
widget
.
maxLines
,
minLines:
widget
.
minLines
,
expands:
widget
.
expands
,
strutStyle:
widget
.
strutStyle
,
selectionColor:
widget
.
selectionColor
,
textScaleFactor:
widget
.
textScaleFactor
??
MediaQuery
.
textScaleFactorOf
(
context
),
textAlign:
widget
.
textAlign
,
textDirection:
_textDirection
,
locale:
widget
.
locale
,
textHeightBehavior:
widget
.
textHeightBehavior
??
DefaultTextHeightBehavior
.
of
(
context
),
textWidthBasis:
widget
.
textWidthBasis
,
obscuringCharacter:
widget
.
obscuringCharacter
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
smartDashesType:
widget
.
smartDashesType
,
smartQuotesType:
widget
.
smartQuotesType
,
enableSuggestions:
widget
.
enableSuggestions
,
offset:
offset
,
onCaretChanged:
_handleCaretChanged
,
rendererIgnoresPointer:
widget
.
rendererIgnoresPointer
,
cursorWidth:
widget
.
cursorWidth
,
cursorHeight:
widget
.
cursorHeight
,
cursorRadius:
widget
.
cursorRadius
,
cursorOffset:
widget
.
cursorOffset
??
Offset
.
zero
,
selectionHeightStyle:
widget
.
selectionHeightStyle
,
selectionWidthStyle:
widget
.
selectionWidthStyle
,
paintCursorAboveText:
widget
.
paintCursorAboveText
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
&&
(!
widget
.
readOnly
||
!
widget
.
obscureText
),
textSelectionDelegate:
this
,
devicePixelRatio:
_devicePixelRatio
,
promptRectRange:
_currentPromptRectRange
,
promptRectColor:
widget
.
autocorrectionTextRectColor
,
clipBehavior:
widget
.
clipBehavior
,
),
),
),
);
...
...
@@ -3122,6 +3247,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
return
TextSpan
(
style:
widget
.
style
,
text:
text
);
}
if
(
_placeholderLocation
>=
0
&&
_placeholderLocation
<=
_value
.
text
.
length
)
{
final
List
<
_ScribblePlaceholder
>
placeholders
=
<
_ScribblePlaceholder
>[];
final
int
placeholderLocation
=
_value
.
text
.
length
-
_placeholderLocation
;
if
(
_isMultiline
)
{
// The zero size placeholder here allows the line to break and keep the caret on the first line.
placeholders
.
add
(
const
_ScribblePlaceholder
(
child:
SizedBox
(),
size:
Size
.
zero
));
placeholders
.
add
(
_ScribblePlaceholder
(
child:
const
SizedBox
(),
size:
Size
(
renderEditable
.
size
.
width
,
0.0
)));
}
else
{
placeholders
.
add
(
const
_ScribblePlaceholder
(
child:
SizedBox
(),
size:
Size
(
100.0
,
0.0
)));
}
return
TextSpan
(
style:
widget
.
style
,
children:
<
InlineSpan
>[
TextSpan
(
text:
_value
.
text
.
substring
(
0
,
placeholderLocation
)),
...
placeholders
,
TextSpan
(
text:
_value
.
text
.
substring
(
placeholderLocation
)),
],
);
}
// Read only mode should not paint text composing.
return
widget
.
controller
.
buildTextSpan
(
context:
context
,
...
...
@@ -3327,6 +3470,142 @@ class _Editable extends MultiChildRenderObjectWidget {
}
}
class
_ScribbleFocusable
extends
StatefulWidget
{
const
_ScribbleFocusable
({
Key
?
key
,
required
this
.
child
,
required
this
.
focusNode
,
required
this
.
editableKey
,
required
this
.
updateSelectionRects
,
required
this
.
enabled
,
}):
super
(
key:
key
);
final
Widget
child
;
final
FocusNode
focusNode
;
final
GlobalKey
editableKey
;
final
VoidCallback
updateSelectionRects
;
final
bool
enabled
;
@override
_ScribbleFocusableState
createState
()
=>
_ScribbleFocusableState
();
}
class
_ScribbleFocusableState
extends
State
<
_ScribbleFocusable
>
implements
ScribbleClient
{
_ScribbleFocusableState
():
_elementIdentifier
=
(
_nextElementIdentifier
++).
toString
();
@override
void
initState
()
{
super
.
initState
();
if
(
widget
.
enabled
)
{
TextInput
.
registerScribbleElement
(
elementIdentifier
,
this
);
}
}
@override
void
didUpdateWidget
(
_ScribbleFocusable
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(!
oldWidget
.
enabled
&&
widget
.
enabled
)
{
TextInput
.
registerScribbleElement
(
elementIdentifier
,
this
);
}
if
(
oldWidget
.
enabled
&&
!
widget
.
enabled
)
{
TextInput
.
unregisterScribbleElement
(
elementIdentifier
);
}
}
@override
void
dispose
()
{
TextInput
.
unregisterScribbleElement
(
elementIdentifier
);
super
.
dispose
();
}
RenderEditable
?
get
renderEditable
=>
widget
.
editableKey
.
currentContext
?.
findRenderObject
()
as
RenderEditable
?;
static
int
_nextElementIdentifier
=
1
;
final
String
_elementIdentifier
;
@override
String
get
elementIdentifier
=>
_elementIdentifier
;
@override
void
onScribbleFocus
(
Offset
offset
)
{
widget
.
focusNode
.
requestFocus
();
renderEditable
?.
selectPositionAt
(
from:
offset
,
cause:
SelectionChangedCause
.
scribble
);
widget
.
updateSelectionRects
();
}
@override
bool
isInScribbleRect
(
Rect
rect
)
{
final
Rect
calculatedBounds
=
bounds
;
if
(
renderEditable
?.
readOnly
??
false
)
return
false
;
if
(
calculatedBounds
==
Rect
.
zero
)
return
false
;
if
(!
calculatedBounds
.
overlaps
(
rect
))
return
false
;
final
Rect
intersection
=
calculatedBounds
.
intersect
(
rect
);
final
HitTestResult
result
=
HitTestResult
();
WidgetsBinding
.
instance
?.
hitTest
(
result
,
intersection
.
center
);
return
result
.
path
.
any
((
HitTestEntry
entry
)
=>
entry
.
target
==
renderEditable
);
}
@override
Rect
get
bounds
{
final
RenderBox
?
box
=
context
.
findRenderObject
()
as
RenderBox
?;
if
(
box
==
null
||
!
mounted
||
!
box
.
attached
)
return
Rect
.
zero
;
final
Matrix4
transform
=
box
.
getTransformTo
(
null
);
return
MatrixUtils
.
transformRect
(
transform
,
Rect
.
fromLTWH
(
0
,
0
,
box
.
size
.
width
,
box
.
size
.
height
));
}
@override
Widget
build
(
BuildContext
context
)
{
return
widget
.
child
;
}
}
class
_ScribblePlaceholder
extends
WidgetSpan
{
const
_ScribblePlaceholder
({
required
Widget
child
,
ui
.
PlaceholderAlignment
alignment
=
ui
.
PlaceholderAlignment
.
bottom
,
TextBaseline
?
baseline
,
TextStyle
?
style
,
required
this
.
size
,
})
:
assert
(
child
!=
null
),
assert
(
baseline
!=
null
||
!(
identical
(
alignment
,
ui
.
PlaceholderAlignment
.
aboveBaseline
)
||
identical
(
alignment
,
ui
.
PlaceholderAlignment
.
belowBaseline
)
||
identical
(
alignment
,
ui
.
PlaceholderAlignment
.
baseline
)
)),
super
(
alignment:
alignment
,
baseline:
baseline
,
style:
style
,
child:
child
,
);
/// The size of the span, used in place of adding a placeholder size to the [TextPainter].
final
Size
size
;
@override
void
build
(
ui
.
ParagraphBuilder
builder
,
{
double
textScaleFactor
=
1.0
,
List
<
PlaceholderDimensions
>?
dimensions
})
{
assert
(
debugAssertIsValid
());
final
bool
hasStyle
=
style
!=
null
;
if
(
hasStyle
)
{
builder
.
pushStyle
(
style
!.
getTextStyle
(
textScaleFactor:
textScaleFactor
));
}
builder
.
addPlaceholder
(
size
.
width
,
size
.
height
,
alignment
,
scale:
textScaleFactor
,
);
if
(
hasStyle
)
{
builder
.
pop
();
}
}
}
/// An interface for retriving the logical text boundary (left-closed-right-open)
/// at a given location in a document.
///
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
c54ce015
...
...
@@ -2454,7 +2454,7 @@ void main() {
);
final
RenderEditable
renderEditable
=
tester
.
renderObject
<
RenderEditable
>(
find
.
byElementPredicate
((
Element
element
)
=>
element
.
renderObject
is
RenderEditable
),
find
.
byElementPredicate
((
Element
element
)
=>
element
.
renderObject
is
RenderEditable
)
.
last
,
);
List
<
TextSelectionPoint
>
lastCharEndpoint
=
renderEditable
.
getEndpointsForSelection
(
...
...
@@ -3252,7 +3252,7 @@ void main() {
expect
(
tester
.
renderObject
<
RenderEditable
>(
find
.
byElementPredicate
((
Element
element
)
=>
element
.
renderObject
is
RenderEditable
),
find
.
byElementPredicate
((
Element
element
)
=>
element
.
renderObject
is
RenderEditable
)
.
last
,
).
text
!.
style
!.
color
,
isSameColorAs
(
CupertinoColors
.
white
),
);
...
...
packages/flutter/test/material/text_field_test.dart
View file @
c54ce015
...
...
@@ -9179,6 +9179,38 @@ void main() {
expect
(
right
.
opacity
.
value
,
equals
(
1.0
));
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
testWidgets
(
'iPad Scribble selection change shows selection handles'
,
(
WidgetTester
tester
)
async
{
const
String
testText
=
'lorem ipsum'
;
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
child:
TextField
(
controller:
controller
,
),
),
),
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
await
tester
.
testTextInput
.
startScribbleInteraction
();
tester
.
testTextInput
.
updateEditingValue
(
const
TextEditingValue
(
text:
testText
,
selection:
TextSelection
(
baseOffset:
2
,
extentOffset:
7
),
));
await
tester
.
pumpAndSettle
();
final
List
<
FadeTransition
>
transitions
=
find
.
byType
(
FadeTransition
).
evaluate
().
map
((
Element
e
)
=>
e
.
widget
).
cast
<
FadeTransition
>().
toList
();
expect
(
transitions
.
length
,
2
);
final
FadeTransition
left
=
transitions
[
0
];
final
FadeTransition
right
=
transitions
[
1
];
expect
(
left
.
opacity
.
value
,
equals
(
1.0
));
expect
(
right
.
opacity
.
value
,
equals
(
1.0
));
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
testWidgets
(
'Tap shows handles but not toolbar'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'abc def ghi'
,
...
...
packages/flutter/test/services/autofill_test.dart
View file @
c54ce015
...
...
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -141,6 +143,21 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
@override
void
autofill
(
TextEditingValue
newEditingValue
)
=>
updateEditingValue
(
newEditingValue
);
@override
void
showToolbar
()
{
latestMethodCall
=
'showToolbar'
;
}
@override
void
insertTextPlaceholder
(
Size
size
)
{
latestMethodCall
=
'insertTextPlaceholder'
;
}
@override
void
removeTextPlaceholder
()
{
latestMethodCall
=
'removeTextPlaceholder'
;
}
}
class
FakeAutofillScope
with
AutofillScopeMixin
implements
AutofillScope
{
...
...
packages/flutter/test/services/delta_text_input_test.dart
View file @
c54ce015
...
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import
'dart:convert'
show
jsonDecode
;
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -114,5 +115,20 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
latestMethodCall
=
'showAutocorrectionPromptRect'
;
}
@override
void
insertTextPlaceholder
(
Size
size
)
{
latestMethodCall
=
'insertTextPlaceholder'
;
}
@override
void
removeTextPlaceholder
()
{
latestMethodCall
=
'removeTextPlaceholder'
;
}
@override
void
showToolbar
()
{
latestMethodCall
=
'showToolbar'
;
}
TextInputConfiguration
get
configuration
=>
const
TextInputConfiguration
(
enableDeltaModel:
true
);
}
packages/flutter/test/services/text_input_test.dart
View file @
c54ce015
...
...
@@ -4,6 +4,7 @@
import
'dart:convert'
show
jsonDecode
;
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -496,6 +497,148 @@ void main() {
expect
(
client
.
latestMethodCall
,
'showAutocorrectionPromptRect'
);
});
test
(
'TextInputClient showToolbar method is called'
,
()
async
{
// Assemble a TextInputConnection so we can verify its change in state.
final
FakeTextInputClient
client
=
FakeTextInputClient
(
TextEditingValue
.
empty
);
const
TextInputConfiguration
configuration
=
TextInputConfiguration
();
TextInput
.
attach
(
client
,
configuration
);
expect
(
client
.
latestMethodCall
,
isEmpty
);
// Send showToolbar message.
final
ByteData
?
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
1
,
0
,
1
],
'method'
:
'TextInputClient.showToolbar'
,
});
await
ServicesBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/textinput'
,
messageBytes
,
(
ByteData
?
_
)
{},
);
expect
(
client
.
latestMethodCall
,
'showToolbar'
);
});
});
group
(
'Scribble interactions'
,
()
{
tearDown
(()
{
TextInputConnection
.
debugResetId
();
});
test
(
'TextInputClient scribbleInteractionBegan and scribbleInteractionFinished'
,
()
async
{
// Assemble a TextInputConnection so we can verify its change in state.
final
FakeTextInputClient
client
=
FakeTextInputClient
(
TextEditingValue
.
empty
);
const
TextInputConfiguration
configuration
=
TextInputConfiguration
();
final
TextInputConnection
connection
=
TextInput
.
attach
(
client
,
configuration
);
expect
(
connection
.
scribbleInProgress
,
false
);
// Send scribbleInteractionBegan message.
ByteData
?
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
1
,
0
,
1
],
'method'
:
'TextInputClient.scribbleInteractionBegan'
,
});
await
ServicesBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/textinput'
,
messageBytes
,
(
ByteData
?
_
)
{},
);
expect
(
connection
.
scribbleInProgress
,
true
);
// Send scribbleInteractionFinished message.
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
1
,
0
,
1
],
'method'
:
'TextInputClient.scribbleInteractionFinished'
,
});
await
ServicesBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/textinput'
,
messageBytes
,
(
ByteData
?
_
)
{},
);
expect
(
connection
.
scribbleInProgress
,
false
);
});
test
(
'TextInputClient focusElement'
,
()
async
{
// Assemble a TextInputConnection so we can verify its change in state.
final
FakeTextInputClient
client
=
FakeTextInputClient
(
TextEditingValue
.
empty
);
const
TextInputConfiguration
configuration
=
TextInputConfiguration
();
TextInput
.
attach
(
client
,
configuration
);
final
FakeScribbleElement
targetElement
=
FakeScribbleElement
(
elementIdentifier:
'target'
);
TextInput
.
registerScribbleElement
(
targetElement
.
elementIdentifier
,
targetElement
);
final
FakeScribbleElement
otherElement
=
FakeScribbleElement
(
elementIdentifier:
'other'
);
TextInput
.
registerScribbleElement
(
otherElement
.
elementIdentifier
,
otherElement
);
expect
(
targetElement
.
latestMethodCall
,
isEmpty
);
expect
(
otherElement
.
latestMethodCall
,
isEmpty
);
// Send focusElement message.
final
ByteData
?
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
targetElement
.
elementIdentifier
,
0.0
,
0.0
],
'method'
:
'TextInputClient.focusElement'
,
});
await
ServicesBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/textinput'
,
messageBytes
,
(
ByteData
?
_
)
{},
);
TextInput
.
unregisterScribbleElement
(
targetElement
.
elementIdentifier
);
TextInput
.
unregisterScribbleElement
(
otherElement
.
elementIdentifier
);
expect
(
targetElement
.
latestMethodCall
,
'onScribbleFocus'
);
expect
(
otherElement
.
latestMethodCall
,
isEmpty
);
});
test
(
'TextInputClient requestElementsInRect'
,
()
async
{
// Assemble a TextInputConnection so we can verify its change in state.
final
FakeTextInputClient
client
=
FakeTextInputClient
(
TextEditingValue
.
empty
);
const
TextInputConfiguration
configuration
=
TextInputConfiguration
();
TextInput
.
attach
(
client
,
configuration
);
final
List
<
FakeScribbleElement
>
targetElements
=
<
FakeScribbleElement
>[
FakeScribbleElement
(
elementIdentifier:
'target1'
,
bounds:
const
Rect
.
fromLTWH
(
0.0
,
0.0
,
100.0
,
100.0
)),
FakeScribbleElement
(
elementIdentifier:
'target2'
,
bounds:
const
Rect
.
fromLTWH
(
0.0
,
100.0
,
100.0
,
100.0
)),
];
final
List
<
FakeScribbleElement
>
otherElements
=
<
FakeScribbleElement
>[
FakeScribbleElement
(
elementIdentifier:
'other1'
,
bounds:
const
Rect
.
fromLTWH
(
100.0
,
0.0
,
100.0
,
100.0
)),
FakeScribbleElement
(
elementIdentifier:
'other2'
,
bounds:
const
Rect
.
fromLTWH
(
100.0
,
100.0
,
100.0
,
100.0
)),
];
void
registerElements
(
FakeScribbleElement
element
)
=>
TextInput
.
registerScribbleElement
(
element
.
elementIdentifier
,
element
);
void
unregisterElements
(
FakeScribbleElement
element
)
=>
TextInput
.
unregisterScribbleElement
(
element
.
elementIdentifier
);
<
FakeScribbleElement
>[...
targetElements
,
...
otherElements
].
forEach
(
registerElements
);
// Send requestElementsInRect message.
final
ByteData
?
messageBytes
=
const
JSONMessageCodec
().
encodeMessage
(<
String
,
dynamic
>{
'args'
:
<
dynamic
>[
0.0
,
50.0
,
50.0
,
100.0
],
'method'
:
'TextInputClient.requestElementsInRect'
,
});
ByteData
?
responseBytes
;
await
ServicesBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
'flutter/textinput'
,
messageBytes
,
(
ByteData
?
response
)
{
responseBytes
=
response
;
},
);
<
FakeScribbleElement
>[...
targetElements
,
...
otherElements
].
forEach
(
unregisterElements
);
final
List
<
List
<
dynamic
>>
responses
=
(
const
JSONMessageCodec
().
decodeMessage
(
responseBytes
)
as
List
<
dynamic
>).
cast
<
List
<
dynamic
>>();
expect
(
responses
.
first
.
length
,
2
);
expect
(
responses
.
first
.
first
,
containsAllInOrder
(<
dynamic
>[
targetElements
.
first
.
elementIdentifier
,
0.0
,
0.0
,
100.0
,
100.0
]));
expect
(
responses
.
first
.
last
,
containsAllInOrder
(<
dynamic
>[
targetElements
.
last
.
elementIdentifier
,
0.0
,
100.0
,
100.0
,
100.0
]));
});
});
test
(
'TextEditingValue.isComposingRangeValid'
,
()
async
{
...
...
@@ -567,5 +710,20 @@ class FakeTextInputClient implements TextInputClient {
latestMethodCall
=
'showAutocorrectionPromptRect'
;
}
@override
void
showToolbar
()
{
latestMethodCall
=
'showToolbar'
;
}
TextInputConfiguration
get
configuration
=>
const
TextInputConfiguration
();
@override
void
insertTextPlaceholder
(
Size
size
)
{
latestMethodCall
=
'insertTextPlaceholder'
;
}
@override
void
removeTextPlaceholder
()
{
latestMethodCall
=
'removeTextPlaceholder'
;
}
}
packages/flutter/test/services/text_input_utils.dart
View file @
c54ce015
...
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import
'dart:convert'
show
utf8
;
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -64,3 +65,29 @@ class FakeTextChannel implements MethodChannel {
}
}
}
class
FakeScribbleElement
implements
ScribbleClient
{
FakeScribbleElement
({
required
String
elementIdentifier
,
Rect
bounds
=
Rect
.
zero
})
:
_elementIdentifier
=
elementIdentifier
,
_bounds
=
bounds
;
final
String
_elementIdentifier
;
final
Rect
_bounds
;
String
latestMethodCall
=
''
;
@override
Rect
get
bounds
=>
_bounds
;
@override
String
get
elementIdentifier
=>
_elementIdentifier
;
@override
bool
isInScribbleRect
(
Rect
rect
)
{
return
_bounds
.
overlaps
(
rect
);
}
@override
void
onScribbleFocus
(
Offset
offset
)
{
latestMethodCall
=
'onScribbleFocus'
;
}
}
packages/flutter/test/widgets/editable_text_test.dart
View file @
c54ce015
...
...
@@ -1775,6 +1775,329 @@ void main() {
}
});
testWidgets
(
'Selection changes during Scribble interaction should have the scribble cause'
,
(
WidgetTester
tester
)
async
{
late
SelectionChangedCause
selectionCause
;
final
TextEditingController
controller
=
TextEditingController
(
text:
'Lorem ipsum dolor sit amet'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
onSelectionChanged:
(
TextSelection
selection
,
SelectionChangedCause
?
cause
)
{
if
(
cause
!=
null
)
selectionCause
=
cause
;
},
),
),
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
// A normal selection update from the framework has 'keyboard' as the cause.
tester
.
testTextInput
.
updateEditingValue
(
TextEditingValue
(
text:
controller
.
text
,
selection:
const
TextSelection
(
baseOffset:
2
,
extentOffset:
3
),
));
await
tester
.
pumpAndSettle
();
expect
(
selectionCause
,
SelectionChangedCause
.
keyboard
);
// A selection update during a scribble interaction has 'scribble' as the cause.
await
tester
.
testTextInput
.
startScribbleInteraction
();
tester
.
testTextInput
.
updateEditingValue
(
TextEditingValue
(
text:
controller
.
text
,
selection:
const
TextSelection
(
baseOffset:
3
,
extentOffset:
4
),
));
await
tester
.
pumpAndSettle
();
expect
(
selectionCause
,
SelectionChangedCause
.
scribble
);
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
testWidgets
(
'Requests focus and changes the selection when onScribbleFocus is called'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Lorem ipsum dolor sit amet'
);
late
SelectionChangedCause
selectionCause
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
onSelectionChanged:
(
TextSelection
selection
,
SelectionChangedCause
?
cause
)
{
if
(
cause
!=
null
)
selectionCause
=
cause
;
},
),
),
);
await
tester
.
testTextInput
.
scribbleFocusElement
(
TextInput
.
scribbleClients
.
keys
.
first
,
Offset
.
zero
);
expect
(
focusNode
.
hasFocus
,
true
);
expect
(
selectionCause
,
SelectionChangedCause
.
scribble
);
// On web, we should rely on the browser's implementation of Scribble, so the selection changed cause
// will never be SelectionChangedCause.scribble.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
testWidgets
(
'Declares itself for Scribble interaction if the bounds overlap the scribble rect and the widget is touchable'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Lorem ipsum dolor sit amet'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
),
),
);
final
List
<
dynamic
>
elementEntry
=
<
dynamic
>[
TextInput
.
scribbleClients
.
keys
.
first
,
0.0
,
0.0
,
800.0
,
600.0
];
List
<
List
<
dynamic
>>
elements
=
await
tester
.
testTextInput
.
scribbleRequestElementsInRect
(
const
Rect
.
fromLTWH
(
0
,
0
,
1
,
1
));
expect
(
elements
.
first
,
containsAll
(
elementEntry
));
// Touch is outside the bounds of the widget.
elements
=
await
tester
.
testTextInput
.
scribbleRequestElementsInRect
(
const
Rect
.
fromLTWH
(-
1
,
-
1
,
1
,
1
));
expect
(
elements
.
length
,
0
);
// Widget is read only.
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
readOnly:
true
,
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
),
),
);
elements
=
await
tester
.
testTextInput
.
scribbleRequestElementsInRect
(
const
Rect
.
fromLTWH
(
0
,
0
,
1
,
1
));
expect
(
elements
.
length
,
0
);
// Widget is not touchable.
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Stack
(
children:
<
Widget
>[
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
),
Positioned
(
left:
0
,
top:
0
,
right:
0
,
bottom:
0
,
child:
Container
(
color:
Colors
.
black
),
),
],
),
),
);
elements
=
await
tester
.
testTextInput
.
scribbleRequestElementsInRect
(
const
Rect
.
fromLTWH
(
0
,
0
,
1
,
1
));
expect
(
elements
.
length
,
0
);
// Widget has scribble disabled.
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
scribbleEnabled:
false
,
),
),
);
elements
=
await
tester
.
testTextInput
.
scribbleRequestElementsInRect
(
const
Rect
.
fromLTWH
(
0
,
0
,
1
,
1
));
expect
(
elements
.
length
,
0
);
// On web, we should rely on the browser's implementation of Scribble, so the engine will
// never request the scribble elements.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
testWidgets
(
'single line Scribble fields can show a horizontal placeholder'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Lorem ipsum dolor sit amet'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
),
),
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
updateEditingValue
(
TextEditingValue
(
text:
controller
.
text
,
selection:
const
TextSelection
(
baseOffset:
5
,
extentOffset:
5
),
));
await
tester
.
pumpAndSettle
();
await
tester
.
testTextInput
.
scribbleInsertPlaceholder
();
await
tester
.
pumpAndSettle
();
TextSpan
textSpan
=
findRenderEditable
(
tester
).
text
!
as
TextSpan
;
expect
(
textSpan
.
children
!.
length
,
3
);
expect
((
textSpan
.
children
![
0
]
as
TextSpan
).
text
,
'Lorem'
);
expect
(
textSpan
.
children
![
1
]
is
WidgetSpan
,
true
);
expect
((
textSpan
.
children
![
2
]
as
TextSpan
).
text
,
' ipsum dolor sit amet'
);
await
tester
.
testTextInput
.
scribbleRemovePlaceholder
();
await
tester
.
pumpAndSettle
();
textSpan
=
findRenderEditable
(
tester
).
text
!
as
TextSpan
;
expect
(
textSpan
.
children
,
null
);
expect
(
textSpan
.
text
,
'Lorem ipsum dolor sit amet'
);
// Widget has scribble disabled.
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
scribbleEnabled:
false
,
),
),
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
updateEditingValue
(
TextEditingValue
(
text:
controller
.
text
,
selection:
const
TextSelection
(
baseOffset:
5
,
extentOffset:
5
),
));
await
tester
.
pumpAndSettle
();
await
tester
.
testTextInput
.
scribbleInsertPlaceholder
();
await
tester
.
pumpAndSettle
();
textSpan
=
findRenderEditable
(
tester
).
text
!
as
TextSpan
;
expect
(
textSpan
.
children
,
null
);
expect
(
textSpan
.
text
,
'Lorem ipsum dolor sit amet'
);
// On web, we should rely on the browser's implementation of Scribble, so the framework
// will not handle placeholders.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
testWidgets
(
'multiline Scribble fields can show a vertical placeholder'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Lorem ipsum dolor sit amet'
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
maxLines:
2
,
),
),
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
updateEditingValue
(
TextEditingValue
(
text:
controller
.
text
,
selection:
const
TextSelection
(
baseOffset:
5
,
extentOffset:
5
),
));
await
tester
.
pumpAndSettle
();
await
tester
.
testTextInput
.
scribbleInsertPlaceholder
();
await
tester
.
pumpAndSettle
();
TextSpan
textSpan
=
findRenderEditable
(
tester
).
text
!
as
TextSpan
;
expect
(
textSpan
.
children
!.
length
,
4
);
expect
((
textSpan
.
children
![
0
]
as
TextSpan
).
text
,
'Lorem'
);
expect
(
textSpan
.
children
![
1
]
is
WidgetSpan
,
true
);
expect
(
textSpan
.
children
![
2
]
is
WidgetSpan
,
true
);
expect
((
textSpan
.
children
![
3
]
as
TextSpan
).
text
,
' ipsum dolor sit amet'
);
await
tester
.
testTextInput
.
scribbleRemovePlaceholder
();
await
tester
.
pumpAndSettle
();
textSpan
=
findRenderEditable
(
tester
).
text
!
as
TextSpan
;
expect
(
textSpan
.
children
,
null
);
expect
(
textSpan
.
text
,
'Lorem ipsum dolor sit amet'
);
// Widget has scribble disabled.
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
controller:
controller
,
backgroundCursorColor:
Colors
.
grey
,
focusNode:
focusNode
,
style:
textStyle
,
cursorColor:
cursorColor
,
selectionControls:
materialTextSelectionControls
,
maxLines:
2
,
scribbleEnabled:
false
,
),
),
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
tester
.
testTextInput
.
updateEditingValue
(
TextEditingValue
(
text:
controller
.
text
,
selection:
const
TextSelection
(
baseOffset:
5
,
extentOffset:
5
),
));
await
tester
.
pumpAndSettle
();
await
tester
.
testTextInput
.
scribbleInsertPlaceholder
();
await
tester
.
pumpAndSettle
();
textSpan
=
findRenderEditable
(
tester
).
text
!
as
TextSpan
;
expect
(
textSpan
.
children
,
null
);
expect
(
textSpan
.
text
,
'Lorem ipsum dolor sit amet'
);
// On web, we should rely on the browser's implementation of Scribble, so the framework
// will not handle placeholders.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
testWidgets
(
'Sends "updateConfig" when read-only flag is flipped'
,
(
WidgetTester
tester
)
async
{
bool
readOnly
=
true
;
late
StateSetter
setState
;
...
...
@@ -3850,6 +4173,85 @@ void main() {
);
});
testWidgets
(
'selection rects are sent when they change'
,
(
WidgetTester
tester
)
async
{
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
SystemChannels
.
textInput
.
setMockMethodCallHandler
((
MethodCall
methodCall
)
async
{
log
.
add
(
methodCall
);
});
final
TextEditingController
controller
=
TextEditingController
();
controller
.
text
=
'Text1'
;
await
tester
.
pumpWidget
(
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
EditableText
(
key:
ValueKey
<
String
>(
controller
.
text
),
controller:
controller
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
),
],
),
),
),
);
await
tester
.
showKeyboard
(
find
.
byKey
(
ValueKey
<
String
>(
controller
.
text
)));
// There should be a new platform message updating the selection rects.
final
MethodCall
methodCall
=
log
.
firstWhere
((
MethodCall
m
)
=>
m
.
method
==
'TextInput.setSelectionRects'
);
expect
(
methodCall
.
method
,
'TextInput.setSelectionRects'
);
expect
((
methodCall
.
arguments
as
List
<
dynamic
>).
length
,
5
);
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
testWidgets
(
'selection rects are not sent if scribbleEnabled is false'
,
(
WidgetTester
tester
)
async
{
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
SystemChannels
.
textInput
.
setMockMethodCallHandler
((
MethodCall
methodCall
)
async
{
log
.
add
(
methodCall
);
});
final
TextEditingController
controller
=
TextEditingController
();
controller
.
text
=
'Text1'
;
await
tester
.
pumpWidget
(
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
EditableText
(
key:
ValueKey
<
String
>(
controller
.
text
),
controller:
controller
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
scribbleEnabled:
false
,
),
],
),
),
),
);
await
tester
.
showKeyboard
(
find
.
byKey
(
ValueKey
<
String
>(
controller
.
text
)));
// There should be a new platform message updating the selection rects.
expect
(
log
.
where
((
MethodCall
m
)
=>
m
.
method
==
'TextInput.setSelectionRects'
).
length
,
0
);
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
testWidgets
(
'text styling info is sent on show keyboard'
,
(
WidgetTester
tester
)
async
{
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
tester
.
binding
.
defaultBinaryMessenger
.
setMockMethodCallHandler
(
SystemChannels
.
textInput
,
(
MethodCall
methodCall
)
async
{
...
...
packages/flutter_test/lib/src/test_text_input.dart
View file @
c54ce015
...
...
@@ -3,6 +3,8 @@
// found in the LICENSE file.
import
'dart:async'
;
import
'dart:typed_data'
;
import
'dart:ui'
show
Rect
,
Offset
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
;
...
...
@@ -271,4 +273,84 @@ class TestTextInput {
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
);
}
/// Simulates a scribble interaction starting.
Future
<
void
>
startScribbleInteraction
()
async
{
assert
(
isRegistered
);
await
TestDefaultBinaryMessengerBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
textInput
.
name
,
SystemChannels
.
textInput
.
codec
.
encodeMethodCall
(
MethodCall
(
'TextInputClient.scribbleInteractionBegan'
,
<
dynamic
>[
_client
??
-
1
,]
),
),
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
);
}
/// Simulates a Scribble focus.
Future
<
void
>
scribbleFocusElement
(
String
elementIdentifier
,
Offset
offset
)
async
{
assert
(
isRegistered
);
await
TestDefaultBinaryMessengerBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
textInput
.
name
,
SystemChannels
.
textInput
.
codec
.
encodeMethodCall
(
MethodCall
(
'TextInputClient.focusElement'
,
<
dynamic
>[
elementIdentifier
,
offset
.
dx
,
offset
.
dy
]
),
),
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
);
}
/// Simulates iOS asking for the list of Scribble elements during UIIndirectScribbleInteraction.
Future
<
List
<
List
<
dynamic
>>>
scribbleRequestElementsInRect
(
Rect
rect
)
async
{
assert
(
isRegistered
);
List
<
List
<
dynamic
>>
response
=
<
List
<
dynamic
>>[];
await
TestDefaultBinaryMessengerBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
textInput
.
name
,
SystemChannels
.
textInput
.
codec
.
encodeMethodCall
(
MethodCall
(
'TextInputClient.requestElementsInRect'
,
<
dynamic
>[
rect
.
left
,
rect
.
top
,
rect
.
width
,
rect
.
height
]
),
),
(
ByteData
?
data
)
{
response
=
(
SystemChannels
.
textInput
.
codec
.
decodeEnvelope
(
data
!)
as
List
<
dynamic
>).
map
((
dynamic
element
)
=>
element
as
List
<
dynamic
>).
toList
();
},
);
return
response
;
}
/// Simulates iOS inserting a UITextPlaceholder during a long press with the pencil.
Future
<
void
>
scribbleInsertPlaceholder
()
async
{
assert
(
isRegistered
);
await
TestDefaultBinaryMessengerBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
textInput
.
name
,
SystemChannels
.
textInput
.
codec
.
encodeMethodCall
(
MethodCall
(
'TextInputClient.insertTextPlaceholder'
,
<
dynamic
>[
_client
??
-
1
,
0.0
,
0.0
]
),
),
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
);
}
/// Simulates iOS removing a UITextPlaceholder after a long press with the pencil is released.
Future
<
void
>
scribbleRemovePlaceholder
()
async
{
assert
(
isRegistered
);
await
TestDefaultBinaryMessengerBinding
.
instance
!.
defaultBinaryMessenger
.
handlePlatformMessage
(
SystemChannels
.
textInput
.
name
,
SystemChannels
.
textInput
.
codec
.
encodeMethodCall
(
MethodCall
(
'TextInputClient.removeTextPlaceholder'
,
<
dynamic
>[
_client
??
-
1
]
),
),
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
);
}
}
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