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
af89a25e
Unverified
Commit
af89a25e
authored
Feb 01, 2022
by
Jami Couch
Committed by
GitHub
Feb 01, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Re-land "Support Scribble Handwriting" (#96615) (#97437)
parent
e5cf7d18
Changes
13
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
1252 additions
and
53 deletions
+1252
-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
+332
-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 @
af89a25e
...
@@ -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
),
...
@@ -468,6 +469,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -468,6 +469,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
),
...
@@ -826,6 +828,9 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -826,6 +828,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
;
...
@@ -871,6 +876,7 @@ class CupertinoTextField extends StatefulWidget {
...
@@ -871,6 +876,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
));
}
}
}
}
...
@@ -991,6 +997,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
...
@@ -991,6 +997,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
;
...
@@ -1320,6 +1329,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
...
@@ -1320,6 +1329,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 @
af89a25e
...
@@ -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
),
...
@@ -781,6 +782,9 @@ class TextField extends StatefulWidget {
...
@@ -781,6 +782,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
;
...
@@ -825,6 +829,7 @@ class TextField extends StatefulWidget {
...
@@ -825,6 +829,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
));
}
}
}
}
...
@@ -1042,7 +1047,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
...
@@ -1042,7 +1047,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
)
...
@@ -1286,6 +1291,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
...
@@ -1286,6 +1291,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 @
af89a25e
...
@@ -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 @
af89a25e
...
@@ -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 @
af89a25e
This diff is collapsed.
Click to expand it.
packages/flutter/test/cupertino/text_field_test.dart
View file @
af89a25e
...
@@ -2454,7 +2454,7 @@ void main() {
...
@@ -2454,7 +2454,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
(
...
@@ -3252,7 +3252,7 @@ void main() {
...
@@ -3252,7 +3252,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 @
af89a25e
...
@@ -9179,6 +9179,38 @@ void main() {
...
@@ -9179,6 +9179,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 @
af89a25e
...
@@ -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 @
af89a25e
...
@@ -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 @
af89a25e
...
@@ -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 @
af89a25e
...
@@ -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 @
af89a25e
This diff is collapsed.
Click to expand it.
packages/flutter_test/lib/src/test_text_input.dart
View file @
af89a25e
...
@@ -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