-
Notifications
You must be signed in to change notification settings - Fork 26.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Framework can receive TextEditingDeltas from engine #88477
Changes from all commits
6896c6e
535c58b
b1c537e
15cb4f4
f65518e
1575424
2c09999
a7d4162
b0de5c9
c674092
1f69ab9
fc800c7
6edb7d3
b1dbf54
390c61e
929f89f
5576bb8
c0e952d
c2ca0c4
c9d0d24
8b2e366
c55debe
99c4b1f
3b823ad
8d8f15b
b2e007e
41ecd4f
9cd5efa
051678c
bb7d24d
636b955
8aac1ea
5938ae1
0b4db8f
c046c53
9daa2ec
559a14c
9c3d545
b70562f
4eeb416
43690fa
eda6fd6
55de79b
36b9e60
392ea4a
5db542d
aca5de8
f16c9d3
3024d46
2e2cb29
f0fe443
995e3d9
30411f9
c2cc8f3
a574926
db8db42
fed90a8
d5318aa
1b1241b
0627ab2
0f553a1
a5e95a1
69a0252
505a06f
586a5d6
3c65b82
af5f856
feb3990
283ca65
60a718e
ec31582
65e8130
967c44c
d4b3e9c
1546d7d
ec45d92
46f30c2
a594423
79b883a
4e7b4c8
5b6183f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ import 'platform_channel.dart'; | |
import 'system_channels.dart'; | ||
import 'system_chrome.dart'; | ||
import 'text_editing.dart'; | ||
import 'text_editing_delta.dart'; | ||
|
||
export 'dart:ui' show TextAffinity; | ||
|
||
|
@@ -467,6 +468,7 @@ class TextInputConfiguration { | |
this.textCapitalization = TextCapitalization.none, | ||
this.autofillConfiguration = AutofillConfiguration.disabled, | ||
this.enableIMEPersonalizedLearning = true, | ||
this.enableDeltaModel = false, | ||
}) : assert(inputType != null), | ||
assert(obscureText != null), | ||
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), | ||
|
@@ -476,7 +478,8 @@ class TextInputConfiguration { | |
assert(keyboardAppearance != null), | ||
assert(inputAction != null), | ||
assert(textCapitalization != null), | ||
assert(enableIMEPersonalizedLearning != null); | ||
assert(enableIMEPersonalizedLearning != null), | ||
assert(enableDeltaModel != null); | ||
|
||
/// The type of information for which to optimize the text input control. | ||
final TextInputType inputType; | ||
|
@@ -622,6 +625,7 @@ class TextInputConfiguration { | |
TextCapitalization? textCapitalization, | ||
bool? enableIMEPersonalizedLearning, | ||
AutofillConfiguration? autofillConfiguration, | ||
bool? enableDeltaModel, | ||
}) { | ||
return TextInputConfiguration( | ||
inputType: inputType ?? this.inputType, | ||
|
@@ -636,8 +640,30 @@ class TextInputConfiguration { | |
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance, | ||
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning, | ||
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration, | ||
enableDeltaModel: enableDeltaModel ?? this.enableDeltaModel, | ||
); | ||
} | ||
|
||
/// Whether to enable that the engine sends text input updates to the | ||
/// framework as [TextEditingDelta]'s or as one [TextEditingValue]. | ||
/// | ||
/// When this is enabled platform text input updates will | ||
/// come through [TextInputClient.updateEditingValueWithDeltas]. | ||
/// | ||
/// When this is disabled platform text input updates will come through | ||
/// [TextInputClient.updateEditingValue]. | ||
/// | ||
/// Enabling this flag results in granular text updates being received from the | ||
/// platforms text input control rather than a single new bulk editing state | ||
/// given by [TextInputClient.updateEditingValue]. | ||
/// | ||
/// If the platform does not currently support the delta model then updates | ||
/// for the editing state will continue to come through the | ||
/// [TextInputClient.updateEditingValue] channel. | ||
/// | ||
/// Defaults to false. Cannot be null. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Should be an empty line above this one:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, is this the place where we should thoroughly explain what this is for? You could mention updateEditingValueWithDeltas and updateEditingValue, and maybe mention that updateEditingValue doesn't work when this is true (is that right?). Long term it would also be cool to have an example of a simple rich text editor or something in the docs somewhere. Or maybe your demo of just showing the current change. |
||
final bool enableDeltaModel; | ||
|
||
/// Returns a representation of this object as a JSON object. | ||
Map<String, dynamic> toJson() { | ||
final Map<String, dynamic>? autofill = autofillConfiguration.toJson(); | ||
|
@@ -655,6 +681,7 @@ class TextInputConfiguration { | |
'keyboardAppearance': keyboardAppearance.toString(), | ||
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning, | ||
if (autofill != null) 'autofill': autofill, | ||
'enableDeltaModel' : enableDeltaModel, | ||
}; | ||
} | ||
} | ||
|
@@ -985,6 +1012,16 @@ abstract class TextInputClient { | |
/// formatting. | ||
void updateEditingValue(TextEditingValue value); | ||
|
||
/// Requests that this client update its editing state by applying the deltas | ||
/// received from the engine. | ||
/// | ||
/// The list of [TextEditingDelta]'s are treated as changes that will be applied | ||
/// to the client's editing state. A change is any mutation to the raw text | ||
/// value, or any updates to the selection and/or composing region. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention opt-in flag here, or people may be confused why this is never called |
||
/// | ||
/// {@macro flutter.services.TextEditingDelta.optIn} | ||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas); | ||
|
||
/// Requests that this client perform the given action. | ||
void performAction(TextInputAction action); | ||
|
||
|
@@ -1476,6 +1513,18 @@ class TextInput { | |
case 'TextInputClient.updateEditingState': | ||
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>)); | ||
break; | ||
case 'TextInputClient.updateEditingStateWithDeltas': | ||
final List<TextEditingDelta> deltas = <TextEditingDelta>[]; | ||
|
||
final Map<String, dynamic> encoded = args[1] as Map<String, dynamic>; | ||
|
||
for (final dynamic encodedDelta in encoded['deltas']) { | ||
final TextEditingDelta delta = TextEditingDelta.fromJSON(encodedDelta as Map<String, dynamic>); | ||
deltas.add(delta); | ||
} | ||
|
||
_currentConnection!._client.updateEditingValueWithDeltas(deltas); | ||
break; | ||
case 'TextInputClient.performAction': | ||
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String)); | ||
break; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1795,6 +1795,15 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
@override | ||
TextEditingValue get currentTextEditingValue => _value; | ||
|
||
@override | ||
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) { | ||
TextEditingValue value = _value; | ||
for (final TextEditingDelta delta in textEditingDeltas) { | ||
value = delta.apply(value); | ||
} | ||
updateEditingValue(value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smart, just call through to updateEditingValue... For a rich text editor author, could they still disable this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A rich text editor author at this low level I think would just implement |
||
} | ||
|
||
@override | ||
void updateEditingValue(TextEditingValue value) { | ||
// This method handles text editing state updates from the platform text | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
// Copyright 2014 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
import 'dart:convert' show jsonDecode; | ||
|
||
import 'package:flutter/services.dart'; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
|
||
void main() { | ||
group('TextEditingDeltaInsertion', () { | ||
test('Verify creation of insertion delta when inserting at a collapsed selection.', () { | ||
const String jsonInsertionDelta = '{' | ||
'"oldText": "",' | ||
' "deltaText": "let there be text",' | ||
' "deltaStart": 0,' | ||
' "deltaEnd": 0,' | ||
' "selectionBase": 17,' | ||
' "selectionExtent": 17,' | ||
' "selectionAffinity" : "TextAffinity.downstream" ,' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": -1,' | ||
' "composingExtent": -1}'; | ||
final TextEditingDeltaInsertion delta = TextEditingDelta.fromJSON(jsonDecode(jsonInsertionDelta) as Map<String, dynamic>) as TextEditingDeltaInsertion; | ||
const TextRange expectedComposing = TextRange.empty; | ||
const int expectedInsertionOffset = 0; | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 17); | ||
|
||
expect(delta.oldText, ''); | ||
expect(delta.textInserted, 'let there be text'); | ||
expect(delta.insertionOffset, expectedInsertionOffset); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
|
||
test('Verify creation of insertion delta when inserting at end of composing region.', () { | ||
const String jsonInsertionDelta = '{' | ||
'"oldText": "hello worl",' | ||
' "deltaText": "world",' | ||
' "deltaStart": 6,' | ||
' "deltaEnd": 10,' | ||
' "selectionBase": 11,' | ||
' "selectionExtent": 11,' | ||
' "selectionAffinity" : "TextAffinity.downstream",' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": 6,' | ||
' "composingExtent": 11}'; | ||
|
||
final TextEditingDeltaInsertion delta = TextEditingDelta.fromJSON(jsonDecode(jsonInsertionDelta) as Map<String, dynamic>) as TextEditingDeltaInsertion; | ||
const TextRange expectedComposing = TextRange(start: 6, end: 11); | ||
const int expectedInsertionOffset = 10; | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 11); | ||
|
||
expect(delta.oldText, 'hello worl'); | ||
expect(delta.textInserted, 'd'); | ||
expect(delta.insertionOffset, expectedInsertionOffset); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
}); | ||
|
||
group('TextEditingDeltaDeletion', () { | ||
test('Verify creation of deletion delta when deleting.', () { | ||
const String jsonDeletionDelta = '{' | ||
'"oldText": "let there be text.",' | ||
' "deltaText": "",' | ||
' "deltaStart": 1,' | ||
' "deltaEnd": 2,' | ||
' "selectionBase": 1,' | ||
' "selectionExtent": 1,' | ||
' "selectionAffinity" : "TextAffinity.downstream" ,' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": -1,' | ||
' "composingExtent": -1}'; | ||
|
||
final TextEditingDeltaDeletion delta = TextEditingDelta.fromJSON(jsonDecode(jsonDeletionDelta) as Map<String, dynamic>) as TextEditingDeltaDeletion; | ||
const TextRange expectedComposing = TextRange.empty; | ||
const TextRange expectedDeletedRange = TextRange(start: 1, end: 2); | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 1); | ||
|
||
expect(delta.oldText, 'let there be text.'); | ||
expect(delta.textDeleted, 'e'); | ||
expect(delta.deletedRange, expectedDeletedRange); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
|
||
test('Verify creation of deletion delta when deleting at end of composing region.', () { | ||
const String jsonDeletionDelta = '{' | ||
'"oldText": "hello world",' | ||
' "deltaText": "worl",' | ||
' "deltaStart": 6,' | ||
' "deltaEnd": 11,' | ||
' "selectionBase": 10,' | ||
' "selectionExtent": 10,' | ||
' "selectionAffinity" : "TextAffinity.downstream",' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": 6,' | ||
' "composingExtent": 10}'; | ||
|
||
final TextEditingDeltaDeletion delta = TextEditingDelta.fromJSON(jsonDecode(jsonDeletionDelta) as Map<String, dynamic>) as TextEditingDeltaDeletion; | ||
const TextRange expectedComposing = TextRange(start: 6, end: 10); | ||
const TextRange expectedDeletedRange = TextRange(start: 10, end: 11); | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 10); | ||
|
||
expect(delta.oldText, 'hello world'); | ||
expect(delta.textDeleted, 'd'); | ||
expect(delta.deletedRange, expectedDeletedRange); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
}); | ||
|
||
group('TextEditingDeltaReplacement', () { | ||
test('Verify creation of replacement delta when replacing with longer.', () { | ||
const String jsonReplacementDelta = '{' | ||
'"oldText": "hello worfi",' | ||
' "deltaText": "working",' | ||
' "deltaStart": 6,' | ||
' "deltaEnd": 11,' | ||
' "selectionBase": 13,' | ||
' "selectionExtent": 13,' | ||
' "selectionAffinity" : "TextAffinity.downstream",' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": 6,' | ||
' "composingExtent": 13}'; | ||
|
||
final TextEditingDeltaReplacement delta = TextEditingDelta.fromJSON(jsonDecode(jsonReplacementDelta) as Map<String, dynamic>) as TextEditingDeltaReplacement; | ||
const TextRange expectedComposing = TextRange(start: 6, end: 13); | ||
const TextRange expectedReplacedRange = TextRange(start: 6, end: 11); | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 13); | ||
|
||
expect(delta.oldText, 'hello worfi'); | ||
expect(delta.textReplaced, 'worfi'); | ||
expect(delta.replacementText, 'working'); | ||
expect(delta.replacedRange, expectedReplacedRange); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
|
||
test('Verify creation of replacement delta when replacing with shorter.', () { | ||
const String jsonReplacementDelta = '{' | ||
'"oldText": "hello world",' | ||
' "deltaText": "h",' | ||
' "deltaStart": 6,' | ||
' "deltaEnd": 11,' | ||
' "selectionBase": 7,' | ||
' "selectionExtent": 7,' | ||
' "selectionAffinity" : "TextAffinity.downstream",' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": 6,' | ||
' "composingExtent": 7}'; | ||
|
||
final TextEditingDeltaReplacement delta = TextEditingDelta.fromJSON(jsonDecode(jsonReplacementDelta) as Map<String, dynamic>) as TextEditingDeltaReplacement; | ||
const TextRange expectedComposing = TextRange(start: 6, end: 7); | ||
const TextRange expectedReplacedRange = TextRange(start: 6, end: 11); | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 7); | ||
|
||
expect(delta.oldText, 'hello world'); | ||
expect(delta.textReplaced, 'world'); | ||
expect(delta.replacementText, 'h'); | ||
expect(delta.replacedRange, expectedReplacedRange); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
|
||
test('Verify creation of replacement delta when replacing with same.', () { | ||
const String jsonReplacementDelta = '{' | ||
'"oldText": "hello world",' | ||
' "deltaText": "words",' | ||
' "deltaStart": 6,' | ||
' "deltaEnd": 11,' | ||
' "selectionBase": 11,' | ||
' "selectionExtent": 11,' | ||
' "selectionAffinity" : "TextAffinity.downstream",' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": 6,' | ||
' "composingExtent": 11}'; | ||
|
||
final TextEditingDeltaReplacement delta = TextEditingDelta.fromJSON(jsonDecode(jsonReplacementDelta) as Map<String, dynamic>) as TextEditingDeltaReplacement; | ||
const TextRange expectedComposing = TextRange(start: 6, end: 11); | ||
const TextRange expectedReplacedRange = TextRange(start: 6, end: 11); | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 11); | ||
|
||
expect(delta.oldText, 'hello world'); | ||
expect(delta.textReplaced, 'world'); | ||
expect(delta.replacementText, 'words'); | ||
expect(delta.replacedRange, expectedReplacedRange); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
}); | ||
|
||
group('TextEditingDeltaNonTextUpdate', () { | ||
test('Verify non text update delta created.', () { | ||
const String jsonNonTextUpdateDelta = '{' | ||
'"oldText": "hello world",' | ||
' "deltaText": "",' | ||
' "deltaStart": -1,' | ||
' "deltaEnd": -1,' | ||
' "selectionBase": 10,' | ||
' "selectionExtent": 10,' | ||
' "selectionAffinity" : "TextAffinity.downstream",' | ||
' "selectionIsDirectional": false,' | ||
' "composingBase": 6,' | ||
' "composingExtent": 11}'; | ||
|
||
final TextEditingDeltaNonTextUpdate delta = TextEditingDelta.fromJSON(jsonDecode(jsonNonTextUpdateDelta) as Map<String, dynamic>) as TextEditingDeltaNonTextUpdate; | ||
const TextRange expectedComposing = TextRange(start: 6, end: 11); | ||
const TextSelection expectedSelection = TextSelection.collapsed(offset: 10); | ||
|
||
expect(delta.oldText, 'hello world'); | ||
expect(delta.selection, expectedSelection); | ||
expect(delta.composing, expectedComposing); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for adding all of these tests! |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May want to also mention that using this results in more granular explicit set of changes rather than batched together general updates to the editing value. We want to help developers find/decide if this feature is right for them.