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
94909174
Unverified
Commit
94909174
authored
Jan 11, 2022
by
Jami Couch
Committed by
GitHub
Jan 11, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support Scribble Handwriting (#75472)
parent
151daec3
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
1249 additions
and
53 deletions
+1249
-53
AUTHORS
AUTHORS
+1
-0
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.
AUTHORS
View file @
94909174
...
@@ -89,3 +89,4 @@ Pradumna Saraf <pradumnasaraf@gmail.com>
...
@@ -89,3 +89,4 @@ Pradumna Saraf <pradumnasaraf@gmail.com>
Kai Yu <yk3372@gmail.com>
Kai Yu <yk3372@gmail.com>
Denis Grafov <grafov.denis@gmail.com>
Denis Grafov <grafov.denis@gmail.com>
TheOneWithTheBraid <the-one@with-the-braid.cf>
TheOneWithTheBraid <the-one@with-the-braid.cf>
Twin Sun, LLC <google-contrib@twinsunsolutions.com>
packages/flutter/lib/src/cupertino/text_field.dart
View file @
94909174
...
@@ -297,6 +297,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -297,6 +297,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
autofillHints
=
const
<
String
>[],
this
.
autofillHints
=
const
<
String
>[],
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
textAlign
!=
null
),
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
readOnly
!=
null
),
...
@@ -454,6 +455,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -454,6 +455,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
autofillHints
=
const
<
String
>[],
this
.
autofillHints
=
const
<
String
>[],
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
textAlign
!=
null
),
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
readOnly
!=
null
),
...
@@ -798,6 +800,9 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -798,6 +800,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.material.textfield.restorationId}
/// {@macro flutter.material.textfield.restorationId}
final
String
?
restorationId
;
final
String
?
restorationId
;
/// {@macro flutter.widgets.editableText.scribbleEnabled}
final
bool
scribbleEnabled
;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final
bool
enableIMEPersonalizedLearning
;
final
bool
enableIMEPersonalizedLearning
;
...
@@ -843,6 +848,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -843,6 +848,7 @@ class CupertinoTextField extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
TextAlignVertical
>(
'textAlignVertical'
,
textAlignVertical
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
TextAlignVertical
>(
'textAlignVertical'
,
textAlignVertical
,
defaultValue:
null
));
properties
.
add
(
EnumProperty
<
TextDirection
>(
'textDirection'
,
textDirection
,
defaultValue:
null
));
properties
.
add
(
EnumProperty
<
TextDirection
>(
'textDirection'
,
textDirection
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Clip
>(
'clipBehavior'
,
clipBehavior
,
defaultValue:
Clip
.
hardEdge
));
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
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
}
}
}
}
...
@@ -963,6 +969,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
...
@@ -963,6 +969,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
if
(
cause
==
SelectionChangedCause
.
keyboard
)
if
(
cause
==
SelectionChangedCause
.
keyboard
)
return
false
;
return
false
;
if
(
cause
==
SelectionChangedCause
.
scribble
)
return
true
;
if
(
_effectiveController
.
text
.
isNotEmpty
)
if
(
_effectiveController
.
text
.
isNotEmpty
)
return
true
;
return
true
;
...
@@ -1292,6 +1301,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
...
@@ -1292,6 +1301,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
autofillClient:
this
,
autofillClient:
this
,
clipBehavior:
widget
.
clipBehavior
,
clipBehavior:
widget
.
clipBehavior
,
restorationId:
'editable'
,
restorationId:
'editable'
,
scribbleEnabled:
widget
.
scribbleEnabled
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
),
),
),
),
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
94909174
...
@@ -329,6 +329,7 @@ class TextField extends StatefulWidget {
...
@@ -329,6 +329,7 @@ class TextField extends StatefulWidget {
this
.
autofillHints
=
const
<
String
>[],
this
.
autofillHints
=
const
<
String
>[],
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
restorationId
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
textAlign
!=
null
),
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
readOnly
!=
null
),
...
@@ -768,6 +769,9 @@ class TextField extends StatefulWidget {
...
@@ -768,6 +769,9 @@ class TextField extends StatefulWidget {
/// {@endtemplate}
/// {@endtemplate}
final
String
?
restorationId
;
final
String
?
restorationId
;
/// {@macro flutter.widgets.editableText.scribbleEnabled}
final
bool
scribbleEnabled
;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final
bool
enableIMEPersonalizedLearning
;
final
bool
enableIMEPersonalizedLearning
;
...
@@ -812,6 +816,7 @@ class TextField extends StatefulWidget {
...
@@ -812,6 +816,7 @@ class TextField extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
ScrollController
>(
'scrollController'
,
scrollController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollController
>(
'scrollController'
,
scrollController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Clip
>(
'clipBehavior'
,
clipBehavior
,
defaultValue:
Clip
.
hardEdge
));
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
));
properties
.
add
(
DiagnosticsProperty
<
bool
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
}
}
}
}
...
@@ -1029,7 +1034,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
...
@@ -1029,7 +1034,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
if
(!
_isEnabled
)
if
(!
_isEnabled
)
return
false
;
return
false
;
if
(
cause
==
SelectionChangedCause
.
longPress
)
if
(
cause
==
SelectionChangedCause
.
longPress
||
cause
==
SelectionChangedCause
.
scribble
)
return
true
;
return
true
;
if
(
_effectiveController
.
text
.
isNotEmpty
)
if
(
_effectiveController
.
text
.
isNotEmpty
)
...
@@ -1273,6 +1278,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
...
@@ -1273,6 +1278,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
autocorrectionTextRectColor:
autocorrectionTextRectColor
,
autocorrectionTextRectColor:
autocorrectionTextRectColor
,
clipBehavior:
widget
.
clipBehavior
,
clipBehavior:
widget
.
clipBehavior
,
restorationId:
'editable'
,
restorationId:
'editable'
,
scribbleEnabled:
widget
.
scribbleEnabled
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
enableIMEPersonalizedLearning:
widget
.
enableIMEPersonalizedLearning
,
),
),
),
),
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
94909174
...
@@ -1265,6 +1265,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
...
@@ -1265,6 +1265,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// [assembleSemanticsNode] invocations.
// [assembleSemanticsNode] invocations.
Queue
<
SemanticsNode
>?
_cachedChildNodes
;
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
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
super
.
describeSemanticsConfiguration
(
config
);
...
...
packages/flutter/lib/src/services/text_input.dart
View file @
94909174
...
@@ -955,6 +955,9 @@ enum SelectionChangedCause {
...
@@ -955,6 +955,9 @@ enum SelectionChangedCause {
/// The user used the mouse to change the selection by dragging over a piece
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
/// of text.
drag
,
drag
,
/// The user used iPadOS 14+ Scribble to change the selection.
scribble
,
}
}
/// A mixin for manipulating the selection, provided for toolbar or shortcut
/// A mixin for manipulating the selection, provided for toolbar or shortcut
...
@@ -1105,6 +1108,76 @@ abstract class TextInputClient {
...
@@ -1105,6 +1108,76 @@ abstract class TextInputClient {
///
///
/// [TextInputClient] should cleanup its connection and finalize editing.
/// [TextInputClient] should cleanup its connection and finalize editing.
void
connectionClosed
();
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].
/// An interface to receive granular information from [TextInput].
...
@@ -1154,6 +1227,7 @@ class TextInputConnection {
...
@@ -1154,6 +1227,7 @@ class TextInputConnection {
Matrix4
?
_cachedTransform
;
Matrix4
?
_cachedTransform
;
Rect
?
_cachedRect
;
Rect
?
_cachedRect
;
Rect
?
_cachedCaretRect
;
Rect
?
_cachedCaretRect
;
List
<
SelectionRect
>
_cachedSelectionRects
=
<
SelectionRect
>[];
static
int
_nextId
=
1
;
static
int
_nextId
=
1
;
final
int
_id
;
final
int
_id
;
...
@@ -1176,6 +1250,12 @@ class TextInputConnection {
...
@@ -1176,6 +1250,12 @@ class TextInputConnection {
/// Whether this connection is currently interacting with the text input control.
/// Whether this connection is currently interacting with the text input control.
bool
get
attached
=>
TextInput
.
_instance
.
_currentConnection
==
this
;
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.
/// Requests that the text input control become visible.
void
show
()
{
void
show
()
{
assert
(
attached
);
assert
(
attached
);
...
@@ -1274,6 +1354,19 @@ class TextInputConnection {
...
@@ -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.
/// Send text styling information.
///
///
/// This information is used by the Flutter Web Engine to change the style
/// This information is used by the Flutter Web Engine to change the style
...
@@ -1535,10 +1628,43 @@ class TextInput {
...
@@ -1535,10 +1628,43 @@ class TextInput {
TextInputConnection
?
_currentConnection
;
TextInputConnection
?
_currentConnection
;
late
TextInputConfiguration
_currentConfiguration
;
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
{
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
)
if
(
_currentConnection
==
null
)
return
;
return
;
final
String
method
=
methodCall
.
method
;
// The requestExistingInputState request needs to be handled regardless of
// The requestExistingInputState request needs to be handled regardless of
// the client ID, as long as we have a _currentConnection.
// the client ID, as long as we have a _currentConnection.
...
@@ -1630,6 +1756,15 @@ class TextInput {
...
@@ -1630,6 +1756,15 @@ class TextInput {
case
'TextInputClient.showAutocorrectionPromptRect'
:
case
'TextInputClient.showAutocorrectionPromptRect'
:
_currentConnection
!.
_client
.
showAutocorrectionPromptRect
(
args
[
1
]
as
int
,
args
[
2
]
as
int
);
_currentConnection
!.
_client
.
showAutocorrectionPromptRect
(
args
[
1
]
as
int
,
args
[
2
]
as
int
);
break
;
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
:
default
:
throw
MissingPluginException
();
throw
MissingPluginException
();
}
}
...
@@ -1703,6 +1838,13 @@ class TextInput {
...
@@ -1703,6 +1838,13 @@ class TextInput {
);
);
}
}
void
_setSelectionRects
(
List
<
List
<
num
>>
args
)
{
_channel
.
invokeMethod
<
void
>(
'TextInput.setSelectionRects'
,
args
,
);
}
void
_setStyle
(
Map
<
String
,
dynamic
>
args
)
{
void
_setStyle
(
Map
<
String
,
dynamic
>
args
)
{
_channel
.
invokeMethod
<
void
>(
_channel
.
invokeMethod
<
void
>(
'TextInput.setStyle'
,
'TextInput.setStyle'
,
...
@@ -1765,4 +1907,18 @@ class TextInput {
...
@@ -1765,4 +1907,18 @@ class TextInput {
shouldSave
,
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 @
94909174
...
@@ -6,7 +6,7 @@ import 'dart:async';
...
@@ -6,7 +6,7 @@ import 'dart:async';
import
'dart:math'
as
math
;
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
hide
TextStyle
;
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/foundation.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/rendering.dart'
;
...
@@ -58,6 +58,10 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
...
@@ -58,6 +58,10 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// is shown in an obscured text field.
// is shown in an obscured text field.
const
int
_kObscureShowLatestCharCursorTicks
=
3
;
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.
/// A controller for an editable text field.
///
///
/// Whenever the user modifies a text field with an associated
/// Whenever the user modifies a text field with an associated
...
@@ -523,6 +527,7 @@ class EditableText extends StatefulWidget {
...
@@ -523,6 +527,7 @@ class EditableText extends StatefulWidget {
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
clipBehavior
=
Clip
.
hardEdge
,
this
.
restorationId
,
this
.
restorationId
,
this
.
scrollBehavior
,
this
.
scrollBehavior
,
this
.
scribbleEnabled
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
this
.
enableIMEPersonalizedLearning
=
true
,
})
:
assert
(
controller
!=
null
),
})
:
assert
(
controller
!=
null
),
assert
(
focusNode
!=
null
),
assert
(
focusNode
!=
null
),
...
@@ -1210,6 +1215,15 @@ class EditableText extends StatefulWidget {
...
@@ -1210,6 +1215,15 @@ class EditableText extends StatefulWidget {
/// [scrollPhysics].
/// [scrollPhysics].
final
ScrollPhysics
?
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}
/// {@template flutter.widgets.editableText.selectionEnabled}
/// Same as [enableInteractiveSelection].
/// Same as [enableInteractiveSelection].
///
///
...
@@ -1510,6 +1524,7 @@ class EditableText extends StatefulWidget {
...
@@ -1510,6 +1524,7 @@ class EditableText extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Iterable
<
String
>>(
'autofillHints'
,
autofillHints
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Iterable
<
String
>>(
'autofillHints'
,
autofillHints
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
TextHeightBehavior
>(
'textHeightBehavior'
,
textHeightBehavior
,
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
>(
'enableIMEPersonalizedLearning'
,
enableIMEPersonalizedLearning
,
defaultValue:
true
));
}
}
}
}
...
@@ -1873,7 +1888,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -1873,7 +1888,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
)
{
if
(
value
.
text
==
_value
.
text
&&
value
.
composing
==
_value
.
composing
)
{
// `selection` is the only change.
// `selection` is the only change.
_handleSelectionChanged
(
value
.
selection
,
SelectionChangedCause
.
keyboard
);
_handleSelectionChanged
(
value
.
selection
,
(
_textInputConnection
?.
scribbleInProgress
??
false
)
?
SelectionChangedCause
.
scribble
:
SelectionChangedCause
.
keyboard
);
}
else
{
}
else
{
hideToolbar
();
hideToolbar
();
_currentPromptRectRange
=
null
;
_currentPromptRectRange
=
null
;
...
@@ -2656,6 +2671,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2656,6 +2671,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Place cursor at the end if the selection is invalid when we receive focus.
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
null
);
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
null
);
}
}
_cachedText
=
''
;
_cachedFirstRect
=
null
;
_cachedSize
=
Size
.
zero
;
_cachedPlaceholder
=
-
1
;
}
else
{
}
else
{
WidgetsBinding
.
instance
!.
removeObserver
(
this
);
WidgetsBinding
.
instance
!.
removeObserver
(
this
);
setState
(()
{
_currentPromptRectRange
=
null
;
});
setState
(()
{
_currentPromptRectRange
=
null
;
});
...
@@ -2663,13 +2683,78 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2663,13 +2683,78 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
updateKeepAlive
();
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
()
{
void
_updateSizeAndTransform
()
{
if
(
_hasInputConnection
)
{
if
(
_hasInputConnection
)
{
final
Size
size
=
renderEditable
.
size
;
final
Size
size
=
renderEditable
.
size
;
final
Matrix4
transform
=
renderEditable
.
getTransformTo
(
null
);
final
Matrix4
transform
=
renderEditable
.
getTransformTo
(
null
);
_textInputConnection
!.
setEditableSizeAndTransform
(
size
,
transform
);
_textInputConnection
!.
setEditableSizeAndTransform
(
size
,
transform
);
_updateSelectionRects
();
SchedulerBinding
.
instance
!
SchedulerBinding
.
instance
!
.
addPostFrameCallback
((
Duration
_
)
=>
_updateSizeAndTransform
());
.
addPostFrameCallback
((
Duration
_
)
=>
_updateSizeAndTransform
());
}
else
if
(
_placeholderLocation
!=
-
1
)
{
removeTextPlaceholder
();
}
}
}
}
...
@@ -2752,6 +2837,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2752,6 +2837,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
///
///
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// is already shown, or when no text selection currently exists.
/// is already shown, or when no text selection currently exists.
@override
bool
showToolbar
()
{
bool
showToolbar
()
{
// Web is using native dom elements to enable clipboard functionality of the
// Web is using native dom elements to enable clipboard functionality of the
// toolbar: copy, paste, select, cut. It might also provide additional
// toolbar: copy, paste, select, cut. It might also provide additional
...
@@ -2790,6 +2876,36 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2790,6 +2876,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
@override
String
get
autofillId
=>
'EditableText-
$hashCode
'
;
String
get
autofillId
=>
'EditableText-
$hashCode
'
;
...
@@ -2991,53 +3107,62 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -2991,53 +3107,62 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onCopy:
_semanticsOnCopy
(
controls
),
onCopy:
_semanticsOnCopy
(
controls
),
onCut:
_semanticsOnCut
(
controls
),
onCut:
_semanticsOnCut
(
controls
),
onPaste:
_semanticsOnPaste
(
controls
),
onPaste:
_semanticsOnPaste
(
controls
),
child:
_Editable
(
child:
_ScribbleFocusable
(
key:
_editableKey
,
focusNode:
widget
.
focusNode
,
startHandleLayerLink:
_startHandleLayerLink
,
editableKey:
_editableKey
,
endHandleLayerLink:
_endHandleLayerLink
,
enabled:
widget
.
scribbleEnabled
,
inlineSpan:
buildTextSpan
(),
updateSelectionRects:
()
{
value:
_value
,
_openInputConnection
();
cursorColor:
_cursorColor
,
_updateSelectionRects
(
force:
true
);
backgroundCursorColor:
widget
.
backgroundCursorColor
,
},
showCursor:
EditableText
.
debugDeterministicCursor
child:
_Editable
(
?
ValueNotifier
<
bool
>(
widget
.
showCursor
)
key:
_editableKey
,
:
_cursorVisibilityNotifier
,
startHandleLayerLink:
_startHandleLayerLink
,
forceLine:
widget
.
forceLine
,
endHandleLayerLink:
_endHandleLayerLink
,
readOnly:
widget
.
readOnly
,
inlineSpan:
buildTextSpan
(),
hasFocus:
_hasFocus
,
value:
_value
,
maxLines:
widget
.
maxLines
,
cursorColor:
_cursorColor
,
minLines:
widget
.
minLines
,
backgroundCursorColor:
widget
.
backgroundCursorColor
,
expands:
widget
.
expands
,
showCursor:
EditableText
.
debugDeterministicCursor
strutStyle:
widget
.
strutStyle
,
?
ValueNotifier
<
bool
>(
widget
.
showCursor
)
selectionColor:
widget
.
selectionColor
,
:
_cursorVisibilityNotifier
,
textScaleFactor:
widget
.
textScaleFactor
??
MediaQuery
.
textScaleFactorOf
(
context
),
forceLine:
widget
.
forceLine
,
textAlign:
widget
.
textAlign
,
readOnly:
widget
.
readOnly
,
textDirection:
_textDirection
,
hasFocus:
_hasFocus
,
locale:
widget
.
locale
,
maxLines:
widget
.
maxLines
,
textHeightBehavior:
widget
.
textHeightBehavior
??
DefaultTextHeightBehavior
.
of
(
context
),
minLines:
widget
.
minLines
,
textWidthBasis:
widget
.
textWidthBasis
,
expands:
widget
.
expands
,
obscuringCharacter:
widget
.
obscuringCharacter
,
strutStyle:
widget
.
strutStyle
,
obscureText:
widget
.
obscureText
,
selectionColor:
widget
.
selectionColor
,
autocorrect:
widget
.
autocorrect
,
textScaleFactor:
widget
.
textScaleFactor
??
MediaQuery
.
textScaleFactorOf
(
context
),
smartDashesType:
widget
.
smartDashesType
,
textAlign:
widget
.
textAlign
,
smartQuotesType:
widget
.
smartQuotesType
,
textDirection:
_textDirection
,
enableSuggestions:
widget
.
enableSuggestions
,
locale:
widget
.
locale
,
offset:
offset
,
textHeightBehavior:
widget
.
textHeightBehavior
??
DefaultTextHeightBehavior
.
of
(
context
),
onCaretChanged:
_handleCaretChanged
,
textWidthBasis:
widget
.
textWidthBasis
,
rendererIgnoresPointer:
widget
.
rendererIgnoresPointer
,
obscuringCharacter:
widget
.
obscuringCharacter
,
cursorWidth:
widget
.
cursorWidth
,
obscureText:
widget
.
obscureText
,
cursorHeight:
widget
.
cursorHeight
,
autocorrect:
widget
.
autocorrect
,
cursorRadius:
widget
.
cursorRadius
,
smartDashesType:
widget
.
smartDashesType
,
cursorOffset:
widget
.
cursorOffset
??
Offset
.
zero
,
smartQuotesType:
widget
.
smartQuotesType
,
selectionHeightStyle:
widget
.
selectionHeightStyle
,
enableSuggestions:
widget
.
enableSuggestions
,
selectionWidthStyle:
widget
.
selectionWidthStyle
,
offset:
offset
,
paintCursorAboveText:
widget
.
paintCursorAboveText
,
onCaretChanged:
_handleCaretChanged
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
,
rendererIgnoresPointer:
widget
.
rendererIgnoresPointer
,
textSelectionDelegate:
this
,
cursorWidth:
widget
.
cursorWidth
,
devicePixelRatio:
_devicePixelRatio
,
cursorHeight:
widget
.
cursorHeight
,
promptRectRange:
_currentPromptRectRange
,
cursorRadius:
widget
.
cursorRadius
,
promptRectColor:
widget
.
autocorrectionTextRectColor
,
cursorOffset:
widget
.
cursorOffset
??
Offset
.
zero
,
clipBehavior:
widget
.
clipBehavior
,
selectionHeightStyle:
widget
.
selectionHeightStyle
,
selectionWidthStyle:
widget
.
selectionWidthStyle
,
paintCursorAboveText:
widget
.
paintCursorAboveText
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
,
textSelectionDelegate:
this
,
devicePixelRatio:
_devicePixelRatio
,
promptRectRange:
_currentPromptRectRange
,
promptRectColor:
widget
.
autocorrectionTextRectColor
,
clipBehavior:
widget
.
clipBehavior
,
),
),
),
),
),
);
);
...
@@ -3067,6 +3192,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3067,6 +3192,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
return
TextSpan
(
style:
widget
.
style
,
text:
text
);
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.
// Read only mode should not paint text composing.
return
widget
.
controller
.
buildTextSpan
(
return
widget
.
controller
.
buildTextSpan
(
context:
context
,
context:
context
,
...
@@ -3271,6 +3414,142 @@ class _Editable extends MultiChildRenderObjectWidget {
...
@@ -3271,6 +3414,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
_bounds
=
bounds
;
if
(
renderEditable
?.
readOnly
??
false
)
return
false
;
if
(
_bounds
==
Rect
.
zero
)
return
false
;
if
(!
_bounds
.
overlaps
(
rect
))
return
false
;
final
Rect
intersection
=
_bounds
.
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)
/// An interface for retriving the logical text boundary (left-closed-right-open)
/// at a given location in a document.
/// at a given location in a document.
///
///
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
94909174
...
@@ -2368,7 +2368,7 @@ void main() {
...
@@ -2368,7 +2368,7 @@ void main() {
);
);
final
RenderEditable
renderEditable
=
tester
.
renderObject
<
RenderEditable
>(
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
(
List
<
TextSelectionPoint
>
lastCharEndpoint
=
renderEditable
.
getEndpointsForSelection
(
...
@@ -3166,7 +3166,7 @@ void main() {
...
@@ -3166,7 +3166,7 @@ void main() {
expect
(
expect
(
tester
.
renderObject
<
RenderEditable
>(
tester
.
renderObject
<
RenderEditable
>(
find
.
byElementPredicate
((
Element
element
)
=>
element
.
renderObject
is
RenderEditable
),
find
.
byElementPredicate
((
Element
element
)
=>
element
.
renderObject
is
RenderEditable
)
.
last
,
).
text
!.
style
!.
color
,
).
text
!.
style
!.
color
,
isSameColorAs
(
CupertinoColors
.
white
),
isSameColorAs
(
CupertinoColors
.
white
),
);
);
...
...
packages/flutter/test/material/text_field_test.dart
View file @
94909174
...
@@ -9255,6 +9255,38 @@ void main() {
...
@@ -9255,6 +9255,38 @@ void main() {
expect
(
right
.
opacity
.
value
,
equals
(
1.0
));
expect
(
right
.
opacity
.
value
,
equals
(
1.0
));
},
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
,
TargetPlatform
.
macOS
}));
},
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
{
testWidgets
(
'Tap shows handles but not toolbar'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
final
TextEditingController
controller
=
TextEditingController
(
text:
'abc def ghi'
,
text:
'abc def ghi'
,
...
...
packages/flutter/test/services/autofill_test.dart
View file @
94909174
...
@@ -2,6 +2,8 @@
...
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
@@ -141,6 +143,21 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
...
@@ -141,6 +143,21 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
@override
@override
void
autofill
(
TextEditingValue
newEditingValue
)
=>
updateEditingValue
(
newEditingValue
);
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
{
class
FakeAutofillScope
with
AutofillScopeMixin
implements
AutofillScope
{
...
...
packages/flutter/test/services/delta_text_input_test.dart
View file @
94909174
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:convert'
show
jsonDecode
;
import
'dart:convert'
show
jsonDecode
;
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
@@ -114,5 +115,20 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
...
@@ -114,5 +115,20 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
latestMethodCall
=
'showAutocorrectionPromptRect'
;
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
);
TextInputConfiguration
get
configuration
=>
const
TextInputConfiguration
(
enableDeltaModel:
true
);
}
}
packages/flutter/test/services/text_input_test.dart
View file @
94909174
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
import
'dart:convert'
show
jsonDecode
;
import
'dart:convert'
show
jsonDecode
;
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
@@ -496,6 +497,148 @@ void main() {
...
@@ -496,6 +497,148 @@ void main() {
expect
(
client
.
latestMethodCall
,
'showAutocorrectionPromptRect'
);
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
{
test
(
'TextEditingValue.isComposingRangeValid'
,
()
async
{
...
@@ -567,5 +710,20 @@ class FakeTextInputClient implements TextInputClient {
...
@@ -567,5 +710,20 @@ class FakeTextInputClient implements TextInputClient {
latestMethodCall
=
'showAutocorrectionPromptRect'
;
latestMethodCall
=
'showAutocorrectionPromptRect'
;
}
}
@override
void
showToolbar
()
{
latestMethodCall
=
'showToolbar'
;
}
TextInputConfiguration
get
configuration
=>
const
TextInputConfiguration
();
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 @
94909174
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:convert'
show
utf8
;
import
'dart:convert'
show
utf8
;
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
@@ -64,3 +65,29 @@ class FakeTextChannel implements MethodChannel {
...
@@ -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 @
94909174
...
@@ -1662,6 +1662,329 @@ void main() {
...
@@ -1662,6 +1662,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
{
testWidgets
(
'Sends "updateConfig" when read-only flag is flipped'
,
(
WidgetTester
tester
)
async
{
bool
readOnly
=
true
;
bool
readOnly
=
true
;
late
StateSetter
setState
;
late
StateSetter
setState
;
...
@@ -3694,6 +4017,85 @@ void main() {
...
@@ -3694,6 +4017,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
{
testWidgets
(
'text styling info is sent on show keyboard'
,
(
WidgetTester
tester
)
async
{
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
tester
.
binding
.
defaultBinaryMessenger
.
setMockMethodCallHandler
(
SystemChannels
.
textInput
,
(
MethodCall
methodCall
)
async
{
tester
.
binding
.
defaultBinaryMessenger
.
setMockMethodCallHandler
(
SystemChannels
.
textInput
,
(
MethodCall
methodCall
)
async
{
...
...
packages/flutter_test/lib/src/test_text_input.dart
View file @
94909174
...
@@ -3,6 +3,8 @@
...
@@ -3,6 +3,8 @@
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:async'
;
import
'dart:async'
;
import
'dart:typed_data'
;
import
'dart:ui'
show
Rect
,
Offset
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
...
@@ -271,4 +273,84 @@ class TestTextInput {
...
@@ -271,4 +273,84 @@ class TestTextInput {
(
ByteData
?
data
)
{
/* response from framework is discarded */
},
(
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