Skip to content
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

TextEditingDelta Support for the Web #28527

Merged
merged 72 commits into from
Feb 10, 2022
Merged
Changes from 10 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
86885dd
Initial implementation of TextEditingDeltaState for the web
Renzo-Olivares Sep 8, 2021
9ef2c43
Capture composing region through compositionupdate and handle cases w…
Renzo-Olivares Sep 9, 2021
b93b043
clean up unused code
Renzo-Olivares Sep 9, 2021
d3201a1
clean up rest of logs
Renzo-Olivares Sep 9, 2021
9f99a66
Make sure we initialize oldText in beforeInput
Renzo-Olivares Sep 9, 2021
8d69317
Clean up comments
Renzo-Olivares Sep 9, 2021
3ba145c
more defaults
Renzo-Olivares Sep 9, 2021
728055a
Add more comments
Renzo-Olivares Sep 9, 2021
efbdf5b
Move delta inferrence logic to TextEditingDeltaState
Renzo-Olivares Sep 9, 2021
1c330f3
Add new listeners to rest of strategies
Renzo-Olivares Sep 9, 2021
0dd249a
Fix existing tests
Renzo-Olivares Sep 13, 2021
37b757e
Fix tests
Renzo-Olivares Sep 13, 2021
fd1396f
Add lastTextEditingDeltaState to test
Renzo-Olivares Sep 13, 2021
cb6ef96
fix tests
Renzo-Olivares Sep 13, 2021
4f610e0
Add some preliminary tests for TextEditingDeltaState
Renzo-Olivares Sep 13, 2021
f772bf1
Send as list to framework
Renzo-Olivares Sep 13, 2021
f6e4d32
Add composing region test
Renzo-Olivares Sep 13, 2021
704d4f9
Address nits
Renzo-Olivares Sep 13, 2021
577aa59
Update tests
Renzo-Olivares Sep 13, 2021
0dcf1f2
Try to fix tests
Renzo-Olivares Sep 13, 2021
a2e5525
Prefer const with constant constructors
Renzo-Olivares Sep 13, 2021
95fe97b
Clean up comments
Renzo-Olivares Sep 13, 2021
5921215
Specify types
Renzo-Olivares Sep 13, 2021
a2a8b9d
fix tests
Renzo-Olivares Sep 14, 2021
1fbab86
Specify type annotations
Renzo-Olivares Sep 14, 2021
928f186
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Renzo-Olivares Jan 11, 2022
03d63c3
batchDeltas -> deltas
Renzo-Olivares Jan 12, 2022
51eabc8
Make eventData nullable so we dont compare with a 'null' string
Renzo-Olivares Jan 12, 2022
125580e
Make TextEditingDeltaState mutable to avoid multiple copies
Renzo-Olivares Jan 12, 2022
26668cb
Fix analyzer
Renzo-Olivares Jan 12, 2022
5a33dbf
Fix test
Renzo-Olivares Jan 13, 2022
e47a3ed
Use safe browser api instead of directly accessing js_util
Renzo-Olivares Jan 13, 2022
04020c2
remove last prefix from editingDeltaState
Renzo-Olivares Jan 19, 2022
13ca351
Remove logs
Renzo-Olivares Jan 19, 2022
73c1411
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Renzo-Olivares Jan 19, 2022
be578c3
fix merge
Renzo-Olivares Jan 22, 2022
2df95b0
fix whitespace
Renzo-Olivares Jan 22, 2022
5e2b294
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Renzo-Olivares Jan 22, 2022
ac3407b
revert composing changes
Renzo-Olivares Jan 24, 2022
fa1d886
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Renzo-Olivares Jan 24, 2022
6ced401
update comments
Renzo-Olivares Jan 25, 2022
15868f6
remove trailing whitespace
Renzo-Olivares Jan 25, 2022
059fe74
Add docs for TextEditingDeltaState
Renzo-Olivares Jan 26, 2022
26e676a
Normalize delta naming and use a copy instead of modifying function a…
Renzo-Olivares Jan 26, 2022
9196226
Update selection of delta in inferDeltaState instead of onChange
Renzo-Olivares Jan 26, 2022
38bc244
Fix tests, previously the selection was not set in inferDeltaState, n…
Renzo-Olivares Jan 26, 2022
cc0bb16
Make a copy of delta instead of modifying function arguments
Renzo-Olivares Jan 26, 2022
b7a91da
remove whitespace
Renzo-Olivares Jan 26, 2022
a600e9e
Move some logic into inferDeltaState
Renzo-Olivares Jan 26, 2022
98fad47
whitespace
Renzo-Olivares Jan 26, 2022
786a528
analyzer fix
Renzo-Olivares Jan 26, 2022
3f260ce
Revert "analyzer fix"
Renzo-Olivares Jan 27, 2022
b9db35a
Revert "whitespace"
Renzo-Olivares Jan 27, 2022
41765f8
Revert "Move some logic into inferDeltaState"
Renzo-Olivares Jan 27, 2022
8ff4f2d
pass _editingDeltaState instead of editingDeltaState to onChange for …
Renzo-Olivares Jan 27, 2022
f4a858a
Add docs for beforeinput
Renzo-Olivares Jan 27, 2022
cbb6b6b
Add docs for inferDeltaState
Renzo-Olivares Jan 27, 2022
b596600
whitespace
Renzo-Olivares Jan 27, 2022
5a8a5f7
Add more docs
Renzo-Olivares Jan 27, 2022
6645a0b
update docs
Renzo-Olivares Jan 27, 2022
da90e99
update docs
Renzo-Olivares Jan 27, 2022
6217ea9
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Renzo-Olivares Jan 27, 2022
b5e0b55
Fix for insertion of a period following a double space within old tex…
Renzo-Olivares Jan 29, 2022
7a5aff9
Fix accent insertion
Jan 31, 2022
c4c9cf6
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Jan 31, 2022
9d97c88
clean up comments
Jan 31, 2022
0fc279d
Address comments for clarity aand regexp
Feb 9, 2022
fc823e9
Make composing and selection nullable
Feb 10, 2022
6bf2ca7
update docs
Feb 10, 2022
acbc0de
whitespace
Feb 10, 2022
7722380
Merge branch 'main' of github.com:flutter/engine into text_editing_de…
Feb 10, 2022
0bb7f30
address comments
Feb 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 211 additions & 5 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';
import 'dart:html' as html;
import 'dart:js_util' as js_util;
import 'dart:math' as math;
import 'dart:typed_data';

Expand Down Expand Up @@ -439,6 +440,142 @@ class AutofillInfo {
}
}

/// Replaces a range of text in the original string with the text given in the
/// replacement string.
String _replace(String originalText, String replacementText, int start, int end) {
final String textStart = originalText.substring(0, start);
final String textEnd = originalText.substring(end, originalText.length);
final String newText = textStart + replacementText + textEnd;
return newText;
}

class TextEditingDeltaState {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining this class, and also a comment below explaining inferDeltaState?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the point of this class is to represent the json that will be sent to the framework, so maybe mention that in the comment.

const TextEditingDeltaState({
this.oldText = '',
this.deltaText = '',
this.deltaStart = -1,
this.deltaEnd = -1,
this.baseOffset = -1,
this.extentOffset = -1,
this.composingOffset = -1,
this.composingExtent = -1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the names slightly different than the frameworks' TextDelta, is it to match how the browser's events present the information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried to match the EditingState parameters in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the empty strings and -1 meaningful values, or are they here only to make the fields non-null? Would it make sense to mark the parameters with required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required wouldn't work in this case since we don't create the entire TextEditingDelta at any single point. It goes through beforeInput -> onInput -> inferDeltaState.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the framework side -1 for deltaStart and deltaEnd is currently used by TextEditingDelta to identify a NonTextUpdate. I made the selection and composing nullable now.

});

static TextEditingDeltaState inferDeltaState(EditingState newEditingState, EditingState? lastEditingState, TextEditingDeltaState? lastTextEditingDeltaState) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make this any simpler if you used inputType? Or is that not possible...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I attempted to use inputType however there is some normalization I do in order to get the behavior of the Web to fit into the processing we do on the framework side that doesn't allow for this.

For example, when trying to use insertCompositionText I discovered it is called for the entire composed word.
So say we have hello| and we delete character by character starting at the end. In this case insertCompositionText is called for every deletion. However on Android & iOS, when we delete the final character h this is reported as a regular deletion, and not one from the composing region.

TextEditingDeltaState newTextEditingDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!);

final bool previousSelectionWasCollapsed = lastEditingState?.baseOffset == lastEditingState?.extentOffset;

if (newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1) {
// We are removing text.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider extracting complex conditions into named variables. Less needs to be explained this way. E.g.:

final bool isTextBeingRemoved = newTextEditingDeltaState.deltaText.isEmpty && newTextEditingDeltaState.deltaEnd != -1;
if (isTextBeingRemoved) {
  ...

final int deletedLength = newTextEditingDeltaState.oldText.length - newEditingState.text!.length;
newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.deltaEnd - deletedLength);
} else if (newTextEditingDeltaState.deltaText.isNotEmpty && !previousSelectionWasCollapsed) {
// We are replacing text at a selection.
newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: lastEditingState!.baseOffset);
}

// If we are composing then set the delta range to the composing region we captured in compositionupdate.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Line length.

final bool isCurrentlyComposing = newTextEditingDeltaState.composingOffset != -1 && newTextEditingDeltaState.composingOffset != newTextEditingDeltaState.composingExtent;
if (newTextEditingDeltaState.deltaText.isNotEmpty && previousSelectionWasCollapsed && isCurrentlyComposing) {
newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: newTextEditingDeltaState.composingOffset, deltaEnd: newTextEditingDeltaState.composingExtent);
}

final bool isDeltaRangeEmpty = newTextEditingDeltaState.deltaStart == -1 && newTextEditingDeltaState.deltaStart == newTextEditingDeltaState.deltaEnd;
if (!isDeltaRangeEmpty) {
// To verify the range of our delta we should compare the newEditingState's
// text with the delta applied to the oldText. If they differ then capture
// the correct delta range from the newEditingState's text value.
//
// We can assume the deltaText for additions and replacements to the text value
// are accurate. What may not be accurate is the range of the delta.
//
// We can think of the newEditingState as our source of truth.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this whole comment explaining the situation of double space inserting a period? Maybe add that as an example in the comment somewhere. And/or, be sure that this is thoroughly tested so that future developers can understand your intention (and not break it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for this case and the accent menu case.

final String textAfterDelta = _replace(
newTextEditingDeltaState.oldText, newTextEditingDeltaState.deltaText,
newTextEditingDeltaState.deltaStart,
newTextEditingDeltaState.deltaEnd);
final bool isDeltaVerified = textAfterDelta == newEditingState.text!;

if (!isDeltaVerified) {
// 1. Find all matches for deltaText.
// 2. Apply matches/replacement to oldText until oldText matches the
// new editing state's text value.
final RegExp deltaTextPattern = RegExp(r'' + newTextEditingDeltaState.deltaText + r'');
for (final Match match in deltaTextPattern.allMatches(newEditingState.text!)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the example of double space inserting a period, the matches here will be all periods in the text, and you're trying to find the one that is new?

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Sep 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the case of double space inserting a period the deltaText we receive from beforeinput is actually . . So we search for a period followed by a whitespace in the new editing state. The new editing state comes directly from the DOM so it can be seen as the source of truth.

When we have the matched ranges we iterate through them and apply the deltaText to the oldText using these matched ranges as the deltaRange. The range that results in our oldText being equal to the value of the new editing state is the correct deltaRange that we should send to the framework.

String textAfterMatch;
int actualEnd;
final bool isMatchWithinOldTextBounds = match.start >= 0 && match.end <= newTextEditingDeltaState.oldText.length;
if (!isMatchWithinOldTextBounds) {
actualEnd = match.start + newTextEditingDeltaState.deltaText.length - 1;
textAfterMatch = _replace(
newTextEditingDeltaState.oldText,
newTextEditingDeltaState.deltaText,
match.start,
actualEnd,
);
} else {
actualEnd = match.end;
textAfterMatch = _replace(
newTextEditingDeltaState.oldText,
newTextEditingDeltaState.deltaText,
match.start,
actualEnd,
);
}

if (textAfterMatch == newEditingState.text!) {
newTextEditingDeltaState = newTextEditingDeltaState.copyWith(deltaStart: match.start, deltaEnd: actualEnd);
break;
}
}
}
}

return newTextEditingDeltaState;
}

final String oldText;
final String deltaText;
final int deltaStart;
final int deltaEnd;
final int baseOffset;
final int extentOffset;
final int composingOffset;
final int composingExtent;

Map<String, dynamic> toFlutter() => <String, dynamic>{
'oldText': oldText,
'deltaText': deltaText,
'deltaStart': deltaStart,
'deltaEnd': deltaEnd,
'selectionBase': baseOffset,
'selectionExtent': extentOffset,
};

TextEditingDeltaState copyWith({
String? oldText,
String? deltaText,
int? deltaStart,
int? deltaEnd,
int? baseOffset,
int? extentOffset,
int? composingOffset,
int? composingExtent,
}) {
return TextEditingDeltaState(
oldText: oldText ?? this.oldText,
deltaText: deltaText ?? this.deltaText,
deltaStart: deltaStart ?? this.deltaStart,
deltaEnd: deltaEnd ?? this.deltaEnd,
baseOffset: baseOffset ?? this.baseOffset,
extentOffset: extentOffset ?? this.extentOffset,
composingOffset: composingOffset ?? this.composingOffset,
composingExtent: composingExtent ?? this.composingExtent,
);
}
}

/// The current text and selection state of a text field.
class EditingState {
EditingState({this.text, int? baseOffset, int? extentOffset}) :
Expand Down Expand Up @@ -610,6 +747,7 @@ class InputConfiguration {
const TextCapitalizationConfig.defaultCapitalization(),
this.autofill,
this.autofillGroup,
this.enableDeltaModel = false,
});

InputConfiguration.fromFrameworkMessage(
Expand All @@ -633,7 +771,8 @@ class InputConfiguration {
autofillGroup = EngineAutofillForm.fromFrameworkMessage(
flutterInputConfiguration.tryJson('autofill'),
flutterInputConfiguration.tryList('fields'),
);
),
enableDeltaModel = flutterInputConfiguration.tryBool('enableDeltaModel') ?? false;

/// The type of information being edited in the input control.
final EngineInputType inputType;
Expand All @@ -658,14 +797,16 @@ class InputConfiguration {
/// supported by Safari.
final bool autocorrect;

final bool enableDeltaModel;

final AutofillInfo? autofill;

final EngineAutofillForm? autofillGroup;

final TextCapitalizationConfig textCapitalization;
}

typedef OnChangeCallback = void Function(EditingState? editingState);
typedef OnChangeCallback = void Function(EditingState? editingState, TextEditingDeltaState? editingDeltaState);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to add this parameter rather than creating a new type as long as nothing is broken by it.

typedef OnActionCallback = void Function(String? inputAction);

/// Provides HTML DOM functionality for editable text.
Expand Down Expand Up @@ -849,6 +990,8 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
late InputConfiguration inputConfiguration;
EditingState? lastEditingState;

TextEditingDeltaState? lastTextEditingDeltaState;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, you are using lastTextEditingDeltaState to accumulate all the delta information during the sequence of events (beforeinput, compositionupdate, input, etc) before flushing the change to the framework. If that's the case, I have a few suggestions:

  1. Make the TextEditingDeltaState class mutable (i.e. remove final from its fields) so you can update the fields in-place instead of creating many copies.
  2. Remove the last prefix, it's confusing because it doesn't mean the same thing as last in lastEditingState.
  3. Since we don't know what's the best time to create this mutable object, I suggest doing something like this:
TextEditingDeltaState? _editingDelta;
TextEditingDeltaState get editingDelta {
  if (_editingDelta == null) {
    _editingDelta = TextEditingDeltaState(oldText: lastEditingState!.text!);
  }
  return _editingDelta!;
}

Then you can simply do:

editingDelta.deltaStart = ...
editingDelta.deltaText = ...

And when the change has been flushed to the framework, just set the delta to null:

_editingDelta = null;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mdebbar thank you for the reviews! I wasn't able to get to this in the past few days but I will come back to it and address all these comments. Thanks again!


/// Styles associated with the editable text.
EditableTextStyle? style;

Expand Down Expand Up @@ -947,6 +1090,10 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {

subscriptions.add(html.document.onSelectionChange.listen(handleChange));

activeDomElement.addEventListener('beforeinput', handleBeforeInput);

activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate);

// Refocus on the activeDomElement after blur, so that user can keep editing the
// text field.
subscriptions.add(activeDomElement.onBlur.listen((_) {
Expand Down Expand Up @@ -978,6 +1125,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {

isEnabled = false;
lastEditingState = null;
lastTextEditingDeltaState = null;
style = null;
geometry = null;

Expand Down Expand Up @@ -1022,13 +1170,40 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
assert(isEnabled);

final EditingState newEditingState = EditingState.fromDomElement(activeDomElement);
final TextEditingDeltaState newTextEditingDeltaState = TextEditingDeltaState.inferDeltaState(newEditingState, lastEditingState, lastTextEditingDeltaState);

if (newEditingState != lastEditingState) {
lastEditingState = newEditingState;
onChange!(lastEditingState);
lastTextEditingDeltaState = newTextEditingDeltaState;
onChange!(lastEditingState, lastTextEditingDeltaState);
// Flush delta after it has been sent to framework.
lastTextEditingDeltaState = TextEditingDeltaState(oldText: lastEditingState!.text!);
}
}

void handleBeforeInput(html.Event event) {
String eventData = js_util.getProperty(event, 'data').toString();
final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!);

if (eventData == 'null') {
// When event.data is 'null' we have a deletion.
// The deltaStart is set in handleChange because there is where we get access
// to the new selection baseOffset which is our new deltaStart.
lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: '', deltaEnd: lastEditingState!.extentOffset);
} else {
// When event.data is not 'null' we we will begin by considering this delta as an insertion
// at the selection extentOffset. This may change due to logic in handleChange to handle
// composition and other IME behaviors.
lastTextEditingDeltaState = newDeltaState.copyWith(oldText: lastEditingState!.text, deltaText: eventData, deltaStart: lastEditingState!.extentOffset, deltaEnd: lastEditingState!.extentOffset);
}
}

void handleCompositionUpdate(html.Event event) {
final EditingState newEditingState = EditingState.fromDomElement(activeDomElement);
final TextEditingDeltaState newDeltaState = lastTextEditingDeltaState ?? TextEditingDeltaState(oldText: lastEditingState!.text!);
lastTextEditingDeltaState = newDeltaState.copyWith(composingOffset: newEditingState.baseOffset, composingExtent: newEditingState.extentOffset);
}

void maybeSendAction(html.Event event) {
if (event is html.KeyboardEvent) {
if (inputConfiguration.inputType.submitActionOnEnter &&
Expand Down Expand Up @@ -1169,6 +1344,10 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {

subscriptions.add(html.document.onSelectionChange.listen(handleChange));

activeDomElement.addEventListener('beforeinput', handleBeforeInput);

activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate);

// Position the DOM element after it is focused.
subscriptions.add(activeDomElement.onFocus.listen((_) {
// Cancel previous timer if exists.
Expand Down Expand Up @@ -1300,6 +1479,10 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {

subscriptions.add(html.document.onSelectionChange.listen(handleChange));

activeDomElement.addEventListener('beforeinput', handleBeforeInput);

activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate);

subscriptions.add(activeDomElement.onBlur.listen((_) {
if (domRenderer.windowHasFocus) {
// Chrome on Android will hide the onscreen keyboard when you tap outside
Expand Down Expand Up @@ -1352,6 +1535,10 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {

subscriptions.add(activeDomElement.onKeyDown.listen(maybeSendAction));

activeDomElement.addEventListener('beforeinput', handleBeforeInput);

activeDomElement.addEventListener('compositionupdate', handleCompositionUpdate);

// Detects changes in text selection.
//
// In Firefox, when cursor moves, neither selectionChange nor onInput
Expand Down Expand Up @@ -1731,6 +1918,20 @@ class TextEditingChannel {
);
}

/// Sends the 'TextInputClient.updateEditingStateWithDeltas' message to the framework.
void updateEditingStateWithDelta(int? clientId, TextEditingDeltaState? editingDeltaState) {
EnginePlatformDispatcher.instance.invokeOnPlatformMessage(
'flutter/textinput',
const JSONMethodCodec().encodeMethodCall(
MethodCall('TextInputClient.updateEditingStateWithDeltas', <dynamic>[
clientId,
editingDeltaState!.toFlutter(),
]),
),
_emptyCallback,
);
}

/// Sends the 'TextInputClient.performAction' message to the framework.
void performAction(int? clientId, String? inputAction) {
EnginePlatformDispatcher.instance.invokeOnPlatformMessage(
Expand Down Expand Up @@ -1824,8 +2025,13 @@ class HybridTextEditing {
isEditing = true;
strategy.enable(
configuration!,
onChange: (EditingState? editingState) {
channel.updateEditingState(_clientId, editingState);
onChange: (EditingState? editingState, TextEditingDeltaState? editingDeltaState) {
if (configuration!.enableDeltaModel) {
editingDeltaState = editingDeltaState!.copyWith(baseOffset: editingState!.baseOffset, extentOffset: editingState.extentOffset);
channel.updateEditingStateWithDelta(_clientId, editingDeltaState);
} else {
channel.updateEditingState(_clientId, editingState);
}
},
onAction: (String? inputAction) {
channel.performAction(_clientId, inputAction);
Expand Down