Skip to content

TextInputClient should receive correct composing range or ignore it on Flutter Web #65357

@znjameswu

Description

@znjameswu

Currently TextInputClient on Flutter Web always receives a TextEditingValue with an empty composing range while the composing text is mixed inside TextEditingValue.text with no hint indicating their nature (The composing range is erased). This behavior is off spec.

This is due to current web engine only listens to onInput event and lacks ability to infer composing range https://github.com/flutter/engine/blob/7d927dd4a4cb1d5a4dc713b5349dfc3578921b97/lib/web_ui/lib/src/engine/text_editing/text_editing.dart#L232

Demo

Details
void main() {
  runApp(MyApp());
}


class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Material App Bar'),
        ),
        body: CustomInput(),
      ),
    );
  }
}

class CustomInput extends StatefulWidget {
  @override
  _CustomInputState createState() => _CustomInputState();
}

class _CustomInputState extends State<CustomInput>
    with AutomaticKeepAliveClientMixin {
  TextInputConnection _textInputConnection;

  FocusNode _focusNode;

  FocusAttachment _focusAttachment;

  InputConnectionController _input;

  void requestKeyboard() {
    if (_focusNode.hasFocus) {
      _input.openConnection(Brightness.dark);
    } else {
      FocusScope.of(context).requestFocus(_focusNode);
    }
  }

  void focusOrUnfocusIfNeeded() {
    if (_focusNode.hasFocus) {
      _focusNode.unfocus();
    }
  }

  @override
  bool get wantKeepAlive => _focusNode.hasFocus;

  void _handleFocusChange() {
    if (_focusNode.hasFocus && _focusNode.consumeKeyboardToken()) {
      _input.openConnection(Brightness.dark);
    } else if (!_focusNode.hasFocus) {
      _input.closeConnection();
    }
    updateKeepAlive();
  }


  bool get _hasFocus => _focusNode.hasFocus;

  bool get _hasInputConnection =>
      _textInputConnection != null && _textInputConnection.attached;

  @override
  void initState() {
    _focusNode = FocusNode();
    super.initState();
    _focusAttachment = _focusNode.attach(context);
    _input = InputConnectionController();
    _focusNode.addListener(_handleFocusChange);
  }

  @override
  void dispose() {
    _focusAttachment.detach();
    _focusNode.removeListener(_handleFocusChange);
    _input.closeConnection();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _focusAttachment.reparent();
    super.build(context); // See AutomaticKeepAliveState.

    return GestureDetector(
      onTap: () => _focusNode.requestFocus(),
      child: Container(
        width: 200,
        height: 200,
        color: Colors.red,
      ),
    );
  }
}

class InputConnectionController implements TextInputClient {
  TextInputConnection _textInputConnection;

  bool get hasConnection =>
      _textInputConnection != null && _textInputConnection.attached;

  void openOrCloseConnection(
      FocusNode focusNode, Brightness keyboardAppearance) {
    if (focusNode.hasFocus && focusNode.consumeKeyboardToken()) {
      openConnection(keyboardAppearance);
    } else if (!focusNode.hasFocus) {
      closeConnection();
    }
  }

  void openConnection(Brightness keyboardAppearance) {
    if (!hasConnection) {
      _textInputConnection = TextInput.attach(
        this,
        TextInputConfiguration(
          inputType: TextInputType.multiline,
          obscureText: false,
          autocorrect: false,
          inputAction: TextInputAction.newline,
          keyboardAppearance: keyboardAppearance,
          textCapitalization: TextCapitalization.sentences,
        ),
      );
    }
    _textInputConnection.show();
  }

  void closeConnection() {
    if (hasConnection) {
      _textInputConnection.close();
      _textInputConnection = null;
    }
  }

  @override
  void connectionClosed() {
    _textInputConnection.connectionClosedReceived();
    _textInputConnection = null;
  }

  @override
  AutofillScope get currentAutofillScope => null;

  @override
  TextEditingValue get currentTextEditingValue => null;

  @override
  void performAction(TextInputAction action) { }

  @override
  void showAutocorrectionPromptRect(int start, int end) {}

  @override
  void updateEditingValue(TextEditingValue value) {
    print('Composing range: ${value.composing.start} to ${value.composing.end}');
  }

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {}

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

When testing on Web, using any IME with composing text feature, the printed composing range will always be -1 to -1.

flutter doctor -v

Details
[√] Flutter (Channel master, 1.22.0-10.0.pre.87, on Microsoft Windows [Version 10.0.19041.450], locale zh-CN)
    • Flutter version 1.22.0-10.0.pre.87 at C:\Users\Zhennan Wu\fvm\versions\master
    • Framework revision 4732a214a7 (3 days ago), 2020-09-04 21:05:02 -0400
    • Engine revision ac8b9c4c52
    • Dart version 2.10.0 (build 2.10.0-86.0.dev)

[√] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at C:\Users\Zhennan Wu\AppData\Local\Android\sdk
    • Platform android-29, build-tools 29.0.2
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.7.1)
    • Visual Studio at C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
    • Visual Studio Community 2019 version 16.7.30406.217
    • Windows 10 SDK version 10.0.18362.0

[√] Android Studio (version 4.0)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin version 48.1.2
    • Dart plugin version 193.7361
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)

[√] IntelliJ IDEA Community Edition (version 2019.3)
    • IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2019.1.3
    • Flutter plugin version 46.0.2
    • Dart plugin version 193.7361

[√] VS Code (version 1.48.2)
    • VS Code at C:\Users\Zhennan Wu\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.14.0

[√] Connected device (4 available)
    • Windows (desktop) • windows    • windows-x64    • Microsoft Windows [Version 10.0.19041.450]
    • Web Server (web)  • web-server • web-javascript • Flutter Tools
    • Chrome (web)      • chrome     • web-javascript • Google Chrome 84.0.4147.135
    • Edge (web)        • edge       • web-javascript • Microsoft Edge 84.0.522.59

• No issues found!

Use case

An erased composing range will cause:

  1. We can no longer infer the Unicode characters that users actually wanted to type from TextEditingValue.text. The composing string itself cannot be filtered and can trigger unwanted logic while the user is actually trying to type something else via their IME.
  2. Make text diffing strategy completely unusable when IME is activated. These strategies are the only solution for problems like Detect when delete is typed into a TextField #14809.

Proposal

Subscribe to compositionstart, compositionend, compositionupdate event in HTML element to either calculate a correct composing range or ignore composing text completely (like current Flutter Windows is doing).

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: text inputEntering text in a text field or keyboard related problemsc: proposalA detailed proposal for a change to Flutterengineflutter/engine related. See also e: labels.good first issueRelatively approachable for first-time contributorsplatform-webWeb applications specifically

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions