Unverified Commit bf66cc2e authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Framework can receive TextEditingDeltas from engine (#88477)

parent 65d8dd98
......@@ -42,6 +42,7 @@ export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart';
export 'src/services/system_sound.dart';
export 'src/services/text_editing.dart';
export 'src/services/text_editing_delta.dart';
export 'src/services/text_formatter.dart';
export 'src/services/text_input.dart';
export 'src/services/text_layout_metrics.dart';
This diff is collapsed.
......@@ -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.
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,
};
}
}
......@@ -956,6 +983,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.
///
/// {@macro flutter.services.TextEditingDelta.optIn}
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
......@@ -1447,6 +1484,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;
......
......@@ -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);
}
@override
void updateEditingValue(TextEditingValue value) {
// This method handles text editing state updates from the platform text
......
......@@ -106,6 +106,19 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
latestMethodCall = 'updateEditingValue';
}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
TextEditingValue newEditingValue = currentTextEditingValue;
for (final TextEditingDelta delta in textEditingDeltas) {
newEditingValue = delta.apply(newEditingValue);
}
currentTextEditingValue = newEditingValue;
latestMethodCall = 'updateEditingValueWithDeltas';
}
@override
AutofillScope? currentAutofillScope;
......
// 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);
});
});
}
......@@ -119,6 +119,7 @@ void main() {
expect(configuration.inputType, TextInputType.text);
expect(configuration.readOnly, false);
expect(configuration.obscureText, false);
expect(configuration.enableDeltaModel, false);
expect(configuration.autocorrect, true);
expect(configuration.actionLabel, null);
expect(configuration.textCapitalization, TextCapitalization.none);
......@@ -450,6 +451,11 @@ class FakeTextInputClient implements TextInputClient {
latestMethodCall = 'updateEditingValue';
}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
latestMethodCall = 'updateEditingValueWithDeltas';
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment