Skip to content

Delete key causes DeltaTextInputClient to send incorrect delta on Web platform #112920

@bleroux

Description

@bleroux

Steps to Reproduce

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const TextInputDeltaApp());
}

// This demo app simulates a very basic text field.
// Edition relies on text editing deltas.
//
// Limitations:
// - Caret and current selection are not displayed.
// - Several keys (delete, backspace, ect) are not handled so they will work only
// on web where they are handled on the engine side.
class TextInputDeltaApp extends StatelessWidget {
  const TextInputDeltaApp({super.key});

  @override
  Widget build(BuildContext context) {
    // MaterialApp is not used here to be as minimal as possible
    // This is possible when running on Web but not on Desktop because
    // many keys (for instance `delete`, `backspace`, etc) are not handled
    // by the engine on Desktop. They are handled by framework shortcuts
    // (see `DefaultTextEditingShortcuts`).
    return const Directionality(
      textDirection: TextDirection.ltr,
      child: TextInputDeltaDemo(),
    );
  }
}

typedef ContentChangedCallback = void Function(String);

class TextInputDeltaDemo extends StatefulWidget {
  const TextInputDeltaDemo({Key? key}) : super(key: key);

  @override
  State<TextInputDeltaDemo> createState() => _TextInputDeltaDemo();
}

class _TextInputDeltaDemo extends State<TextInputDeltaDemo> {
  String content = "Flutter!";
  MinimalTextInputClient? inputClient;

  @override
  void initState() {
    super.initState();
    _attachTextInputClient();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: Center(
            child: Semantics(
              textField: true,
              child: Container(
                color: Colors.amber,
                width: 300,
                height: 100,
                child: Text(content),
              ),
            ),
          ),
        ),
      ],
    );
  }

  void _attachTextInputClient() {
    inputClient ??= MinimalTextInputClient();
    inputClient!.attach(content, (String value) {
      setState(() {
        content = value;
      });
    });
  }
}

class MinimalTextInputClient implements DeltaTextInputClient {
  TextInputConnection? _textInputConnection;
  TextEditingValue _value = TextEditingValue.empty;
  ContentChangedCallback? onChange;

  void attach(String initialText, ContentChangedCallback onChange) {
    this.onChange = onChange;
    _value = TextEditingValue(text: initialText);
    _textInputConnection = TextInput.attach(
      this,
      const TextInputConfiguration(
        enableDeltaModel: true,
        inputAction: TextInputAction.newline,
        inputType: TextInputType.multiline,
      ),
    );
    _textInputConnection!.setEditingState(_value);
    _textInputConnection!.show();
  }

  @override
  void connectionClosed() {
    _textInputConnection!.close();
  }

  @override
  // TODO: implement currentAutofillScope
  AutofillScope? get currentAutofillScope => throw UnimplementedError();

  @override
  // TODO: implement currentTextEditingValue
  TextEditingValue? get currentTextEditingValue => _value;

  @override
  void insertTextPlaceholder(Size size) {
    // TODO: implement insertTextPlaceholder
  }

  @override
  void performAction(TextInputAction action) {
    // TODO: implement performAction
  }

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {
    // TODO: implement performPrivateCommand
  }

  @override
  void performSelector(String selectorName) {
    // TODO: implement performSelector
  }

  @override
  void removeTextPlaceholder() {
    // TODO: implement removeTextPlaceholder
  }

  @override
  void showAutocorrectionPromptRect(int start, int end) {
    // TODO: implement showAutocorrectionPromptRect
  }

  @override
  void showToolbar() {
    // TODO: implement showToolbar
  }

  @override
  void updateEditingValue(TextEditingValue value) {
    // TODO: implement updateEditingValue
  }

  @override
  void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
    for (var delta in textEditingDeltas) {
      _value = delta.apply(_value);
      onChange?.call(_value.text);
    }
  }

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {
    // TODO: implement updateFloatingCursor
  }

  @override
  void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
    // TODO: implement didChangeInputControl
  }
}
  • Run the code sample on Web (flutter run -d chrome).
  • Hit the delete key.

Expected results: the first character is deleted
Actual results: an exception is thrown

Error log
Error: Assertion failed: org-dartlang-sdk:///flutter_web_sdk/lib/_engine/engine/text_editing/text_editing.dart:429:10
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 266:49      throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 29:3        assertFailed
lib/_engine/engine/text_editing/text_editing.dart 429:23                          _replace
lib/_engine/engine/text_editing/text_editing.dart 511:37                          inferDeltaState
lib/_engine/engine/text_editing/text_editing.dart 1290:56                         handleChange

(This error was first reported in flutter/samples#1424 which uses the simplistic_editor sample for reproducible steps).

Metadata

Metadata

Assignees

Labels

a: text inputEntering text in a text field or keyboard related problemsengineflutter/engine related. See also e: labels.found in release: 3.4Found to occur in 3.4has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-webWeb applications specifically

Type

No type

Projects

Status

Done (PR merged)

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions