From eea56e9465958f51bf129a999d58ff1737203331 Mon Sep 17 00:00:00 2001 From: agata Date: Thu, 19 Sep 2024 00:30:15 +0900 Subject: [PATCH] fix: the composing text did not show an underline during IME conversion (#2242) --- .../editor/raw_editor/raw_editor_state.dart | 16 +++++- ..._editor_state_text_input_client_mixin.dart | 19 ++++++- lib/src/editor/widgets/text/text_block.dart | 4 ++ lib/src/editor/widgets/text/text_line.dart | 54 ++++++++++++++++++- 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index 6cdd6f6a0..0e98d25f7 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -674,6 +674,7 @@ class QuillRawEditorState extends EditorState widget.configurations.customRecognizerBuilder, customStyleBuilder: widget.configurations.customStyleBuilder, customLinkPrefixes: widget.configurations.customLinkPrefixes, + composingRange: composingRange.value, ); result.add( Directionality( @@ -720,7 +721,9 @@ class QuillRawEditorState extends EditorState _hasFocus, MediaQuery.devicePixelRatioOf(context), _cursorCont, - _styles!.inlineCode!); + _styles!.inlineCode!, + composingRange.value, + _styles!.paragraph!.style.color!); return editableTextLine; } @@ -850,6 +853,9 @@ class QuillRawEditorState extends EditorState _floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController.addListener(onFloatingCursorResetTick); + // listen to composing range changes + composingRange.addListener(_onComposingRangeChanged); + if (isKeyboardOS) { _keyboardVisible = true; } else if (!kIsWeb && isFlutterTest) { @@ -987,6 +993,7 @@ class QuillRawEditorState extends EditorState controller.removeListener(_didChangeTextEditingValueListener); widget.configurations.focusNode.removeListener(_handleFocusChanged); _cursorCont.dispose(); + composingRange.removeListener(_onComposingRangeChanged); if (_clipboardStatus != null) { _clipboardStatus! ..removeListener(_onChangedClipboardStatus) @@ -999,6 +1006,13 @@ class QuillRawEditorState extends EditorState _selectionOverlay?.updateForScroll(); } + void _onComposingRangeChanged() { + if (!mounted) { + return; + } + _markNeedsBuild(); + } + /// Marks the editor as dirty and trigger a rebuild. /// /// When the editor is dirty methods that depend on the editor diff --git a/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart index f1132bf03..99f99a7b4 100644 --- a/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -2,7 +2,7 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/animation.dart' show Curves; import 'package:flutter/cupertino.dart' show CupertinoTheme; -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/foundation.dart' show ValueNotifier, kIsWeb; import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; @@ -15,7 +15,22 @@ import 'raw_editor.dart'; mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClient { TextInputConnection? _textInputConnection; - TextEditingValue? _lastKnownRemoteTextEditingValue; + TextEditingValue? __lastKnownRemoteTextEditingValue; + + set _lastKnownRemoteTextEditingValue(TextEditingValue? value) { + __lastKnownRemoteTextEditingValue = value; + if (composingRange.value != value?.composing) { + composingRange.value = value?.composing ?? TextRange.empty; + } + } + + TextEditingValue? get _lastKnownRemoteTextEditingValue => + __lastKnownRemoteTextEditingValue; + + /// The range of text that is currently being composed. + final ValueNotifier composingRange = ValueNotifier( + TextRange.empty, + ); /// Whether to create an input connection with the platform for text editing /// or not. diff --git a/lib/src/editor/widgets/text/text_block.dart b/lib/src/editor/widgets/text/text_block.dart index 8208ab0f0..292db507e 100644 --- a/lib/src/editor/widgets/text/text_block.dart +++ b/lib/src/editor/widgets/text/text_block.dart @@ -78,6 +78,7 @@ class EditableTextBlock extends StatelessWidget { required this.onCheckboxTap, required this.readOnly, required this.customRecognizerBuilder, + required this.composingRange, this.checkBoxReadOnly, this.onLaunchUrl, this.customStyleBuilder, @@ -111,6 +112,7 @@ class EditableTextBlock extends StatelessWidget { final bool readOnly; final bool? checkBoxReadOnly; final List customLinkPrefixes; + final TextRange composingRange; @override Widget build(BuildContext context) { @@ -204,6 +206,8 @@ class EditableTextBlock extends StatelessWidget { MediaQuery.devicePixelRatioOf(context), cursorCont, styles!.inlineCode!, + composingRange, + styles!.paragraph!.style.color!, ); final nodeTextDirection = getDirectionOfNode(line, textDirection); children.add( diff --git a/lib/src/editor/widgets/text/text_line.dart b/lib/src/editor/widgets/text/text_line.dart index 2e5dd9778..daa402e9e 100644 --- a/lib/src/editor/widgets/text/text_line.dart +++ b/lib/src/editor/widgets/text/text_line.dart @@ -643,6 +643,8 @@ class EditableTextLine extends RenderObjectWidget { this.devicePixelRatio, this.cursorCont, this.inlineCodeStyle, + this.composingRange, + this.composingColor, {super.key}); final Line line; @@ -658,6 +660,8 @@ class EditableTextLine extends RenderObjectWidget { final double devicePixelRatio; final CursorCont cursorCont; final InlineCodeStyle inlineCodeStyle; + final TextRange composingRange; + final Color composingColor; @override RenderObjectElement createElement() { @@ -676,7 +680,9 @@ class EditableTextLine extends RenderObjectWidget { _getPadding(), color, cursorCont, - inlineCodeStyle); + inlineCodeStyle, + composingRange, + composingColor); } @override @@ -692,7 +698,8 @@ class EditableTextLine extends RenderObjectWidget { ..hasFocus = hasFocus ..setDevicePixelRatio(devicePixelRatio) ..setCursorCont(cursorCont) - ..setInlineCodeStyle(inlineCodeStyle); + ..setInlineCodeStyle(inlineCodeStyle) + ..setComposingRange(composingRange); } EdgeInsetsGeometry _getPadding() { @@ -719,6 +726,8 @@ class RenderEditableTextLine extends RenderEditableBox { this.color, this.cursorCont, this.inlineCodeStyle, + this.composingRange, + this.composingColor, ); RenderBox? _leading; @@ -737,6 +746,8 @@ class RenderEditableTextLine extends RenderEditableBox { List? _selectedRects; late Rect _caretPrototype; InlineCodeStyle inlineCodeStyle; + TextRange composingRange; + Color composingColor; final Map children = {}; Iterable get _children sync* { @@ -852,6 +863,12 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsLayout(); } + void setComposingRange(TextRange newComposingRange) { + if (composingRange == newComposingRange) return; + composingRange = newComposingRange; + markNeedsLayout(); + } + // Start selection implementation bool containsTextSelection() { @@ -1334,6 +1351,11 @@ class RenderEditableTextLine extends RenderEditableBox { _paintSelection(context, effectiveOffset); } + + // Paints an underline to indicate the text being composed by the IME. + if (composingRange.isValid) { + _paintComposing(context); + } } } @@ -1365,6 +1387,34 @@ class RenderEditableTextLine extends RenderEditableBox { ); } + // Paints a line below the composing text. + void _paintComposing(PaintingContext context) { + assert(composingRange.isValid); + final composingStart = composingRange.start - line.documentOffset; + final composingEnd = composingRange.end - line.documentOffset; + if (composingStart < 0 || composingEnd < 0) { + return; + } + final composingRects = _body!.getBoxesForSelection( + TextSelection( + baseOffset: composingStart, + extentOffset: composingEnd, + ), + ); + final paint = Paint() + ..color = composingColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + for (final box in composingRects) { + final rect = box.toRect(); + context.canvas.drawLine( + rect.bottomLeft.translate(0, -5), + rect.bottomRight.translate(0, -5), + paint, + ); + } + } + @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { if (_leading != null) {