Unverified Commit 4b330ddb authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Create DeltaTextInputClient (#90205)

* Create DeltaTextInputClient

* Remove old tests as updateEditingValueWithDeltas is no longer implemented

* fix analyzer

* Update docs

* Make example more general

* Update docs

* Add assert to check that TextInputClient is a DeltaTextInputClient

* Update assert

* More docs

* update

* Clean up docs

* updates

* Update docs

* updates

* Fix test

* add test

* updates

* remove logs

* fix tests

* Address reviewer comments

* Add text_input_utils.dart

* Address reviewer comments
parent b509dc04
......@@ -35,10 +35,10 @@ String _replace(String originalText, String replacementText, int start, int end)
/// * [TextEditingDeltaDeletion], a delta representing a deletion.
/// * [TextEditingDeltaReplacement], a delta representing a replacement.
/// * [TextEditingDeltaNonTextUpdate], a delta representing an update to the
/// selection and/or composing region.
/// * [TextInputConfiguration], to opt-in your [TextInputClient] to receive
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
/// selection and/or composing region.
/// * [TextInputConfiguration], to opt-in your [DeltaTextInputClient] to receive
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
abstract class TextEditingDelta {
/// Creates a delta for a given change to the editing state.
///
......@@ -234,9 +234,9 @@ class TextEditingDeltaInsertion extends TextEditingDelta {
/// {@template flutter.services.TextEditingDelta.optIn}
/// See also:
///
/// * [TextInputConfiguration], to opt-in your [TextInputClient] to receive
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
/// * [TextInputConfiguration], to opt-in your [DeltaTextInputClient] to receive
/// [TextEditingDelta]'s you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
/// {@endtemplate}
const TextEditingDeltaInsertion({
required String oldText,
......
......@@ -647,19 +647,24 @@ class TextInputConfiguration {
/// 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.
/// platform's text input control.
///
/// When this is enabled:
/// * You must implement [DeltaTextInputClient] and not [TextInputClient] to
/// receive granular updates from the platform's text input.
/// * Platform text input updates will come through
/// [DeltaTextInputClient.updateEditingValueWithDeltas].
/// * If [TextInputClient] is implemented with this property enabled then
/// you will experience unexpected behavior as [TextInputClient] does not implement
/// a delta channel.
///
/// When this is disabled:
/// * If [DeltaTextInputClient] is implemented then updates for the
/// editing state will continue to come through the
/// [DeltaTextInputClient.updateEditingValue] channel.
/// * If [TextInputClient] is implemented then updates for the editing
/// state will come through [TextInputClient.updateEditingValue].
///
/// Defaults to false. Cannot be null.
final bool enableDeltaModel;
......@@ -953,10 +958,15 @@ mixin TextSelectionDelegate {
/// An interface to receive information from [TextInput].
///
/// If [TextInputConfiguration.enableDeltaModel] is set to true,
/// [DeltaTextInputClient] must be implemented instead of this class.
///
/// See also:
///
/// * [TextInput.attach]
/// * [EditableText], a [TextInputClient] implementation.
/// * [DeltaTextInputClient], a [TextInputClient] extension that receives
/// granular information from the platform's text input.
abstract class TextInputClient {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
......@@ -983,16 +993,6 @@ 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);
......@@ -1026,6 +1026,36 @@ abstract class TextInputClient {
void connectionClosed();
}
/// An interface to receive granular information from [TextInput].
///
/// See also:
///
/// * [TextInput.attach]
/// * [TextInputConfiguration], to opt-in to receive [TextEditingDelta]'s from
/// the platforms [TextInput] you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
abstract class DeltaTextInputClient extends TextInputClient {
/// 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.
///
/// Here is an example of what implementation of this method could look like:
/// {@tool snippet}
/// @override
/// void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
/// TextEditingValue newValue = _previousValue;
/// for (final TextEditingDelta delta in textEditingDeltas) {
/// newValue = delta.apply(newValue);
/// }
/// _localValue = newValue;
/// }
/// {@end-tool}
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
}
/// An interface for interacting with a text input control.
///
/// See also:
......@@ -1485,6 +1515,7 @@ class TextInput {
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
break;
case 'TextInputClient.updateEditingStateWithDeltas':
assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true');
final List<TextEditingDelta> deltas = <TextEditingDelta>[];
final Map<String, dynamic> encoded = args[1] as Map<String, dynamic>;
......@@ -1494,7 +1525,7 @@ class TextInput {
deltas.add(delta);
}
_currentConnection!._client.updateEditingValueWithDeltas(deltas);
(_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
break;
case 'TextInputClient.performAction':
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
......
......@@ -1795,15 +1795,6 @@ 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
......
......@@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show utf8;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'text_input_utils.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
......@@ -106,19 +106,6 @@ 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;
......@@ -169,62 +156,3 @@ class FakeAutofillScope with AutofillScopeMixin implements AutofillScope {
clients.putIfAbsent(client.autofillId, () => client);
}
}
class FakeTextChannel implements MethodChannel {
FakeTextChannel(this.outgoing) : assert(outgoing != null);
Future<dynamic> Function(MethodCall) outgoing;
Future<void> Function(MethodCall)? incoming;
List<MethodCall> outgoingCalls = <MethodCall>[];
@override
BinaryMessenger get binaryMessenger => throw UnimplementedError();
@override
MethodCodec get codec => const JSONMethodCodec();
@override
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
final MethodCall call = MethodCall(method, arguments);
outgoingCalls.add(call);
return await outgoing(call) as T;
}
@override
String get name => 'flutter/textinput';
@override
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) {
incoming = handler;
}
void validateOutgoingMethodCalls(List<MethodCall> calls) {
expect(outgoingCalls.length, calls.length);
bool hasError = false;
for (int i = 0; i < calls.length; i++) {
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
if (outgoingString != expectedString) {
print(
'Index $i did not match:\n'
' actual: ${outgoingCalls[i]}\n'
' expected: ${calls[i]}',
);
hasError = true;
}
}
if (hasError) {
fail('Calls did not match.');
}
}
}
// 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';
import 'text_input_utils.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('DeltaTextInputClient', () {
late FakeTextChannel fakeTextChannel;
setUp(() {
fakeTextChannel = FakeTextChannel((MethodCall call) async {});
TextInput.setChannel(fakeTextChannel);
});
tearDown(() {
TextInputConnection.debugResetId();
TextInput.setChannel(SystemChannels.textInput);
});
test(
'DeltaTextInputClient send the correct configuration to the platform and responds to updateEditingValueWithDeltas method correctly',
() async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeDeltaTextInputClient client = FakeDeltaTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration(enableDeltaModel: true);
TextInput.attach(client, configuration);
expect(client.configuration.enableDeltaModel, true);
expect(client.latestMethodCall, isEmpty);
const String jsonDelta = '{'
'"oldText": "",'
' "deltaText": "let there be text",'
' "deltaStart": 0,'
' "deltaEnd": 0,'
' "selectionBase": 17,'
' "selectionExtent": 17,'
' "selectionAffinity" : "TextAffinity.downstream" ,'
' "selectionIsDirectional": false,'
' "composingBase": -1,'
' "composingExtent": -1}';
// Send updateEditingValueWithDeltas message.
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[
1,
jsonDecode('{"deltas": [$jsonDelta]}'),
],
'method': 'TextInputClient.updateEditingStateWithDeltas',
});
await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(client.latestMethodCall, 'updateEditingValueWithDeltas');
},
);
});
}
class FakeDeltaTextInputClient implements DeltaTextInputClient {
FakeDeltaTextInputClient(this.currentTextEditingValue);
String latestMethodCall = '';
@override
TextEditingValue currentTextEditingValue;
@override
AutofillScope? get currentAutofillScope => null;
@override
void performAction(TextInputAction action) {
latestMethodCall = 'performAction';
}
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {
latestMethodCall = 'performPrivateCommand';
}
@override
void updateEditingValue(TextEditingValue value) {
latestMethodCall = 'updateEditingValue';
}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
latestMethodCall = 'updateEditingValueWithDeltas';
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
}
@override
void connectionClosed() {
latestMethodCall = 'connectionClosed';
}
@override
void showAutocorrectionPromptRect(int start, int end) {
latestMethodCall = 'showAutocorrectionPromptRect';
}
TextInputConfiguration get configuration => const TextInputConfiguration(enableDeltaModel: true);
}
......@@ -3,12 +3,13 @@
// found in the LICENSE file.
import 'dart:convert' show utf8;
import 'dart:convert' show jsonDecode;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'text_input_utils.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
......@@ -451,11 +452,6 @@ class FakeTextInputClient implements TextInputClient {
latestMethodCall = 'updateEditingValue';
}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
latestMethodCall = 'updateEditingValueWithDeltas';
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
......@@ -473,60 +469,3 @@ class FakeTextInputClient implements TextInputClient {
TextInputConfiguration get configuration => const TextInputConfiguration();
}
class FakeTextChannel implements MethodChannel {
FakeTextChannel(this.outgoing) : assert(outgoing != null);
Future<dynamic> Function(MethodCall) outgoing;
Future<void> Function(MethodCall)? incoming;
List<MethodCall> outgoingCalls = <MethodCall>[];
@override
BinaryMessenger get binaryMessenger => throw UnimplementedError();
@override
MethodCodec get codec => const JSONMethodCodec();
@override
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
final MethodCall call = MethodCall(method, arguments);
outgoingCalls.add(call);
return await outgoing(call) as T;
}
@override
String get name => 'flutter/textinput';
@override
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
void validateOutgoingMethodCalls(List<MethodCall> calls) {
expect(outgoingCalls.length, calls.length);
bool hasError = false;
for (int i = 0; i < calls.length; i++) {
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
if (outgoingString != expectedString) {
print(
'Index $i did not match:\n'
' actual: $outgoingString\n'
' expected: $expectedString',
);
hasError = true;
}
}
if (hasError) {
fail('Calls did not match.');
}
}
}
// 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 utf8;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeTextChannel implements MethodChannel {
FakeTextChannel(this.outgoing) : assert(outgoing != null);
Future<dynamic> Function(MethodCall) outgoing;
Future<void> Function(MethodCall)? incoming;
List<MethodCall> outgoingCalls = <MethodCall>[];
@override
BinaryMessenger get binaryMessenger => throw UnimplementedError();
@override
MethodCodec get codec => const JSONMethodCodec();
@override
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
final MethodCall call = MethodCall(method, arguments);
outgoingCalls.add(call);
return await outgoing(call) as T;
}
@override
String get name => 'flutter/textinput';
@override
void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
void validateOutgoingMethodCalls(List<MethodCall> calls) {
expect(outgoingCalls.length, calls.length);
bool hasError = false;
for (int i = 0; i < calls.length; i++) {
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
if (outgoingString != expectedString) {
print(
'Index $i did not match:\n'
' actual: $outgoingString\n'
' expected: $expectedString',
);
hasError = true;
}
}
if (hasError) {
fail('Calls did not match.');
}
}
}
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