Unverified Commit 1148a2a8 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Migrate EditableTextState from addPostFrameCallbacks to compositionCallbacks (#119359)

* PostFrameCallbacks -> compositionCallbacks

* review

* review
parent 865dc5c5
...@@ -168,9 +168,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -168,9 +168,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
assert(delta != 0); assert(delta != 0);
_compositionCallbackCount += delta; _compositionCallbackCount += delta;
assert(_compositionCallbackCount >= 0); assert(_compositionCallbackCount >= 0);
if (parent != null) { parent?._updateSubtreeCompositionObserverCount(delta);
parent!._updateSubtreeCompositionObserverCount(delta);
}
} }
void _fireCompositionCallbacks({required bool includeChildren}) { void _fireCompositionCallbacks({required bool includeChildren}) {
......
...@@ -84,6 +84,51 @@ const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500); ...@@ -84,6 +84,51 @@ const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500);
// is shown in an obscured text field. // is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3; const int _kObscureShowLatestCharCursorTicks = 3;
class _CompositionCallback extends SingleChildRenderObjectWidget {
const _CompositionCallback({ required this.compositeCallback, required this.enabled, super.child });
final CompositionCallback compositeCallback;
final bool enabled;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderCompositionCallback(compositeCallback, enabled);
}
@override
void updateRenderObject(BuildContext context, _RenderCompositionCallback renderObject) {
super.updateRenderObject(context, renderObject);
// _EditableTextState always uses the same callback.
assert(renderObject.compositeCallback == compositeCallback);
renderObject.enabled = enabled;
}
}
class _RenderCompositionCallback extends RenderProxyBox {
_RenderCompositionCallback(this.compositeCallback, this._enabled);
final CompositionCallback compositeCallback;
VoidCallback? _cancelCallback;
bool get enabled => _enabled;
bool _enabled = false;
set enabled(bool newValue) {
_enabled = newValue;
if (!newValue) {
_cancelCallback?.call();
_cancelCallback = null;
} else if (_cancelCallback == null) {
markNeedsPaint();
}
}
@override
void paint(PaintingContext context, ui.Offset offset) {
if (enabled) {
_cancelCallback ??= context.addCompositionCallback(compositeCallback);
}
super.paint(context, offset);
}
}
/// A controller for an editable text field. /// A controller for an editable text field.
/// ///
/// Whenever the user modifies a text field with an associated /// Whenever the user modifies a text field with an associated
...@@ -2970,8 +3015,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2970,8 +3015,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration) ? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration)
: TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration); : TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
_updateSizeAndTransform(); _updateSizeAndTransform();
_updateComposingRectIfNeeded(); _schedulePeriodicPostFrameCallbacks();
_updateCaretRectIfNeeded();
final TextStyle style = widget.style; final TextStyle style = widget.style;
_textInputConnection! _textInputConnection!
..setStyle( ..setStyle(
...@@ -2999,6 +3043,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2999,6 +3043,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_textInputConnection!.close(); _textInputConnection!.close();
_textInputConnection = null; _textInputConnection = null;
_lastKnownRemoteTextEditingValue = null; _lastKnownRemoteTextEditingValue = null;
removeTextPlaceholder();
} }
} }
...@@ -3523,6 +3568,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3523,6 +3568,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
updateKeepAlive(); updateKeepAlive();
} }
void _compositeCallback(Layer layer) {
// The callback can be invoked when the layer is detached.
// The input connection can be closed by the platform in which case this
// widget doesn't rebuild.
if (!renderEditable.attached || !_hasInputConnection) {
return;
}
assert(mounted);
assert((context as Element).debugIsActive);
_updateSizeAndTransform();
}
void _updateSizeAndTransform() {
final Size size = renderEditable.size;
final Matrix4 transform = renderEditable.getTransformTo(null);
_textInputConnection!.setEditableSizeAndTransform(size, transform);
}
void _schedulePeriodicPostFrameCallbacks([Duration? duration]) {
if (!_hasInputConnection) {
return;
}
_updateSelectionRects();
_updateComposingRectIfNeeded();
_updateCaretRectIfNeeded();
SchedulerBinding.instance.addPostFrameCallback(_schedulePeriodicPostFrameCallbacks);
}
_ScribbleCacheKey? _scribbleCacheKey; _ScribbleCacheKey? _scribbleCacheKey;
void _updateSelectionRects({bool force = false}) { void _updateSelectionRects({bool force = false}) {
...@@ -3585,18 +3657,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3585,18 +3657,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_textInputConnection!.setSelectionRects(rects); _textInputConnection!.setSelectionRects(rects);
} }
void _updateSizeAndTransform() {
if (_hasInputConnection) {
final Size size = renderEditable.size;
final Matrix4 transform = renderEditable.getTransformTo(null);
_textInputConnection!.setEditableSizeAndTransform(size, transform);
_updateSelectionRects();
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
} else if (_placeholderLocation != -1) {
removeTextPlaceholder();
}
}
// Sends the current composing rect to the iOS text input plugin via the text // Sends the current composing rect to the iOS text input plugin via the text
// input channel. We need to keep sending the information even if no text is // input channel. We need to keep sending the information even if no text is
// currently marked, as the information usually lags behind. The text input // currently marked, as the information usually lags behind. The text input
...@@ -3604,42 +3664,34 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3604,42 +3664,34 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// when the composing rect info didn't arrive in time. // when the composing rect info didn't arrive in time.
void _updateComposingRectIfNeeded() { void _updateComposingRectIfNeeded() {
final TextRange composingRange = _value.composing; final TextRange composingRange = _value.composing;
if (_hasInputConnection) { assert(mounted);
assert(mounted); Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
Rect? composingRect = renderEditable.getRectForComposingRange(composingRange); // Send the caret location instead if there's no marked text yet.
// Send the caret location instead if there's no marked text yet. if (composingRect == null) {
if (composingRect == null) { assert(!composingRange.isValid || composingRange.isCollapsed);
assert(!composingRange.isValid || composingRange.isCollapsed); final int offset = composingRange.isValid ? composingRange.start : 0;
final int offset = composingRange.isValid ? composingRange.start : 0; composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
}
_textInputConnection!.setComposingRect(composingRect);
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateComposingRectIfNeeded());
} }
_textInputConnection!.setComposingRect(composingRect);
} }
void _updateCaretRectIfNeeded() { void _updateCaretRectIfNeeded() {
if (_hasInputConnection) { final TextSelection? selection = renderEditable.selection;
if (renderEditable.selection != null && renderEditable.selection!.isValid && if (selection == null || !selection.isValid || !selection.isCollapsed) {
renderEditable.selection!.isCollapsed) { return;
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_textInputConnection!.setCaretRect(caretRect);
}
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateCaretRectIfNeeded());
} }
final TextPosition currentTextPosition = TextPosition(offset: selection.baseOffset);
final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_textInputConnection!.setCaretRect(caretRect);
} }
TextDirection get _textDirection { TextDirection get _textDirection => widget.textDirection ?? Directionality.of(context);
final TextDirection result = widget.textDirection ?? Directionality.of(context);
return result;
}
/// The renderer for this widget's descendant. /// The renderer for this widget's descendant.
/// ///
/// This property is typically used to notify the renderer of input gestures /// This property is typically used to notify the renderer of input gestures
/// when [RenderEditable.ignorePointer] is true. /// when [RenderEditable.ignorePointer] is true.
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable; late final RenderEditable renderEditable = _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@override @override
TextEditingValue get textEditingValue => _value; TextEditingValue get textEditingValue => _value;
...@@ -3812,7 +3864,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3812,7 +3864,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void removeTextPlaceholder() { void removeTextPlaceholder() {
if (!widget.scribbleEnabled) { if (!widget.scribbleEnabled || _placeholderLocation == -1) {
return; return;
} }
...@@ -4243,100 +4295,104 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -4243,100 +4295,104 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls? controls = widget.selectionControls; final TextSelectionControls? controls = widget.selectionControls;
return TextFieldTapRegion( return _CompositionCallback(
onTapOutside: widget.onTapOutside ?? _defaultOnTapOutside, compositeCallback: _compositeCallback,
debugLabel: kReleaseMode ? null : 'EditableText', enabled: _hasInputConnection,
child: MouseRegion( child: TextFieldTapRegion(
cursor: widget.mouseCursor ?? SystemMouseCursors.text, onTapOutside: widget.onTapOutside ?? _defaultOnTapOutside,
child: Actions( debugLabel: kReleaseMode ? null : 'EditableText',
actions: _actions, child: MouseRegion(
child: _TextEditingHistory( cursor: widget.mouseCursor ?? SystemMouseCursors.text,
controller: widget.controller, child: Actions(
onTriggered: (TextEditingValue value) { actions: _actions,
userUpdateTextEditingValue(value, SelectionChangedCause.keyboard); child: _TextEditingHistory(
}, controller: widget.controller,
child: Focus( onTriggered: (TextEditingValue value) {
focusNode: widget.focusNode, userUpdateTextEditingValue(value, SelectionChangedCause.keyboard);
includeSemantics: false, },
debugLabel: kReleaseMode ? null : 'EditableText', child: Focus(
child: Scrollable( focusNode: widget.focusNode,
key: _scrollableKey, includeSemantics: false,
excludeFromSemantics: true, debugLabel: kReleaseMode ? null : 'EditableText',
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, child: Scrollable(
controller: _scrollController, key: _scrollableKey,
physics: widget.scrollPhysics, excludeFromSemantics: true,
dragStartBehavior: widget.dragStartBehavior, axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
restorationId: widget.restorationId, controller: _scrollController,
// If a ScrollBehavior is not provided, only apply scrollbars when physics: widget.scrollPhysics,
// multiline. The overscroll indicator should not be applied in dragStartBehavior: widget.dragStartBehavior,
// either case, glowing or stretching. restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith( // If a ScrollBehavior is not provided, only apply scrollbars when
scrollbars: _isMultiline, // multiline. The overscroll indicator should not be applied in
overscroll: false, // either case, glowing or stretching.
), scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(
viewportBuilder: (BuildContext context, ViewportOffset offset) { scrollbars: _isMultiline,
return CompositedTransformTarget( overscroll: false,
link: _toolbarLayerLink, ),
child: Semantics( viewportBuilder: (BuildContext context, ViewportOffset offset) {
onCopy: _semanticsOnCopy(controls), return CompositedTransformTarget(
onCut: _semanticsOnCut(controls), link: _toolbarLayerLink,
onPaste: _semanticsOnPaste(controls), child: Semantics(
child: _ScribbleFocusable( onCopy: _semanticsOnCopy(controls),
focusNode: widget.focusNode, onCut: _semanticsOnCut(controls),
editableKey: _editableKey, onPaste: _semanticsOnPaste(controls),
enabled: widget.scribbleEnabled, child: _ScribbleFocusable(
updateSelectionRects: () { focusNode: widget.focusNode,
_openInputConnection(); editableKey: _editableKey,
_updateSelectionRects(force: true); enabled: widget.scribbleEnabled,
}, updateSelectionRects: () {
child: _Editable( _openInputConnection();
key: _editableKey, _updateSelectionRects(force: true);
startHandleLayerLink: _startHandleLayerLink, },
endHandleLayerLink: _endHandleLayerLink, child: _Editable(
inlineSpan: buildTextSpan(), key: _editableKey,
value: _value, startHandleLayerLink: _startHandleLayerLink,
cursorColor: _cursorColor, endHandleLayerLink: _endHandleLayerLink,
backgroundCursorColor: widget.backgroundCursorColor, inlineSpan: buildTextSpan(),
showCursor: EditableText.debugDeterministicCursor value: _value,
? ValueNotifier<bool>(widget.showCursor) cursorColor: _cursorColor,
: _cursorVisibilityNotifier, backgroundCursorColor: widget.backgroundCursorColor,
forceLine: widget.forceLine, showCursor: EditableText.debugDeterministicCursor
readOnly: widget.readOnly, ? ValueNotifier<bool>(widget.showCursor)
hasFocus: _hasFocus, : _cursorVisibilityNotifier,
maxLines: widget.maxLines, forceLine: widget.forceLine,
minLines: widget.minLines, readOnly: widget.readOnly,
expands: widget.expands, hasFocus: _hasFocus,
strutStyle: widget.strutStyle, maxLines: widget.maxLines,
selectionColor: widget.selectionColor, minLines: widget.minLines,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), expands: widget.expands,
textAlign: widget.textAlign, strutStyle: widget.strutStyle,
textDirection: _textDirection, selectionColor: widget.selectionColor,
locale: widget.locale, textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), textAlign: widget.textAlign,
textWidthBasis: widget.textWidthBasis, textDirection: _textDirection,
obscuringCharacter: widget.obscuringCharacter, locale: widget.locale,
obscureText: widget.obscureText, textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context),
offset: offset, textWidthBasis: widget.textWidthBasis,
onCaretChanged: _handleCaretChanged, obscuringCharacter: widget.obscuringCharacter,
rendererIgnoresPointer: widget.rendererIgnoresPointer, obscureText: widget.obscureText,
cursorWidth: widget.cursorWidth, offset: offset,
cursorHeight: widget.cursorHeight, onCaretChanged: _handleCaretChanged,
cursorRadius: widget.cursorRadius, rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorOffset: widget.cursorOffset ?? Offset.zero, cursorWidth: widget.cursorWidth,
selectionHeightStyle: widget.selectionHeightStyle, cursorHeight: widget.cursorHeight,
selectionWidthStyle: widget.selectionWidthStyle, cursorRadius: widget.cursorRadius,
paintCursorAboveText: widget.paintCursorAboveText, cursorOffset: widget.cursorOffset ?? Offset.zero,
enableInteractiveSelection: widget._userSelectionEnabled, selectionHeightStyle: widget.selectionHeightStyle,
textSelectionDelegate: this, selectionWidthStyle: widget.selectionWidthStyle,
devicePixelRatio: _devicePixelRatio, paintCursorAboveText: widget.paintCursorAboveText,
promptRectRange: _currentPromptRectRange, enableInteractiveSelection: widget._userSelectionEnabled,
promptRectColor: widget.autocorrectionTextRectColor, textSelectionDelegate: this,
clipBehavior: widget.clipBehavior, devicePixelRatio: _devicePixelRatio,
promptRectRange: _currentPromptRectRange,
promptRectColor: widget.autocorrectionTextRectColor,
clipBehavior: widget.clipBehavior,
),
), ),
), ),
), );
); },
}, ),
), ),
), ),
), ),
......
...@@ -179,6 +179,9 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex ...@@ -179,6 +179,9 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex
painter(this, offset); painter(this, offset);
} }
@override
VoidCallback addCompositionCallback(CompositionCallback callback) => () {};
@override @override
void noSuchMethod(Invocation invocation) { } void noSuchMethod(Invocation invocation) { }
} }
......
...@@ -14611,6 +14611,66 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ...@@ -14611,6 +14611,66 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
// Shouldn't crash. // Shouldn't crash.
state.didChangeMetrics(); state.didChangeMetrics();
}); });
testWidgets('_CompositionCallback widget does not skip frames', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(selection: TextSelection.collapsed(offset: 0)),
);
Offset offset = Offset.zero;
late StateSetter setState;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return Transform.translate(
offset: offset,
// The EditableText is configured in a way that the it doesn't
// explicitly request repaint on focus change.
child: TickerMode(
enabled: false,
child: RepaintBoundary(
child: EditableText(
controller: controller,
focusNode: focusNode,
style: const TextStyle(),
showCursor: false,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
),
);
}
),
),
);
focusNode.requestFocus();
await tester.pump();
tester.testTextInput.log.clear();
// The composition callback should be registered. To verify, change the
// parent layer's transform.
setState(() { offset = const Offset(42, 0); });
await tester.pump();
expect(
tester.testTextInput.log,
contains(
matchesMethodCall(
'TextInput.setEditableSizeAndTransform',
args: containsPair('transform', Matrix4.translationValues(offset.dx, offset.dy, 0).storage),
),
),
);
EditableText.debugDeterministicCursor = false;
});
} }
class UnsettableController extends TextEditingController { class UnsettableController extends TextEditingController {
......
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