Unverified Commit 3cde69e8 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Revert "Revert "Scribble mixin (#104128)" (#114647)" (#114698)

Relands the "Scribble mixin" PR, which was reverted due to breaking a Google test in the last roll.
Breaks the Scribble feature out of TextInputClient in order to avoid breaking changes.
parent e9018323
......@@ -37,6 +37,7 @@ export 'src/services/raw_keyboard_macos.dart';
export 'src/services/raw_keyboard_web.dart';
export 'src/services/raw_keyboard_windows.dart';
export 'src/services/restoration.dart';
export 'src/services/scribble.dart';
export 'src/services/service_extensions.dart';
export 'src/services/spell_check.dart';
export 'src/services/system_channels.dart';
......
......@@ -15,9 +15,9 @@ import 'binary_messenger.dart';
import 'hardware_keyboard.dart';
import 'message_codec.dart';
import 'restoration.dart';
import 'scribble.dart';
import 'service_extensions.dart';
import 'system_channels.dart';
import 'text_input.dart';
export 'dart:ui' show ChannelBuffers, RootIsolateToken;
......@@ -43,7 +43,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage);
TextInput.ensureInitialized();
Scribble.ensureInitialized();
readInitialLifecycleStateFromNativeWindow();
}
......@@ -326,7 +326,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
void setSystemUiChangeCallback(SystemUiChangeCallback? callback) {
_systemUiChangeCallback = callback;
}
}
/// Signature for listening to changes in the [SystemUiMode].
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
/// An interface into system-level handwriting text input.
///
/// This is typically used by implemeting the methods in [ScribbleClient] in a
/// class, usually a [State], and setting an instance of it to [client]. The
/// relevant methods on [ScribbleClient] will be called in response to method
/// channel calls on [SystemChannels.scribble].
///
/// Currently, handwriting input is supported in the iOS embedder with the Apple
/// Pencil.
///
/// [EditableText] uses this class via [ScribbleClient] to automatically support
/// handwriting input when [EditableText.scribbleEnabled] is set to true.
///
/// See also:
///
/// * [SystemChannels.scribble], which is the [MethodChannel] used by this
/// class, and which has a list of the methods that this class handles.
class Scribble {
Scribble._() {
_channel.setMethodCallHandler(_handleScribbleInvocation);
}
/// Ensure that a [Scribble] instance has been set up so that the platform
/// can handle messages on the scribble method channel.
static void ensureInitialized() {
_instance; // ignore: unnecessary_statements
}
/// Set the [MethodChannel] used to communicate with the system's text input
/// control.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to do handwriting input. This has no effect if
/// asserts are disabled.
@visibleForTesting
static void setChannel(MethodChannel newChannel) {
assert(() {
_instance._channel = newChannel..setMethodCallHandler(_instance._handleScribbleInvocation);
return true;
}());
}
static final Scribble _instance = Scribble._();
/// Set the given [ScribbleClient] as the single active client.
///
/// This is usually based on the [ScribbleClient] receiving focus.
static set client(ScribbleClient? client) {
_instance._client = client;
}
/// Return the current active [ScribbleClient], or null if none.
static ScribbleClient? get client => _instance._client;
ScribbleClient? _client;
MethodChannel _channel = SystemChannels.scribble;
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
bool _scribbleInProgress = false;
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
@visibleForTesting
static Map<String, ScribbleClient> get scribbleClients => Scribble._instance._scribbleClients;
/// Returns true if a scribble interaction is currently happening.
static bool get scribbleInProgress => _instance._scribbleInProgress;
Future<dynamic> _handleScribbleInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
if (method == 'Scribble.focusElement') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
return;
} else if (method == 'Scribble.requestElementsInRect') {
final List<double> args = (methodCall.arguments as List<dynamic>).cast<num>().map<double>((num value) => value.toDouble()).toList();
return _scribbleClients.keys.where((String elementIdentifier) {
final Rect rect = Rect.fromLTWH(args[0], args[1], args[2], args[3]);
if (!(_scribbleClients[elementIdentifier]?.isInScribbleRect(rect) ?? false)) {
return false;
}
final Rect bounds = _scribbleClients[elementIdentifier]?.bounds ?? Rect.zero;
return !(bounds == Rect.zero || bounds.hasNaN || bounds.isInfinite);
}).map((String elementIdentifier) {
final Rect bounds = _scribbleClients[elementIdentifier]!.bounds;
return <dynamic>[elementIdentifier, ...<dynamic>[bounds.left, bounds.top, bounds.width, bounds.height]];
}).toList();
} else if (method == 'Scribble.scribbleInteractionBegan') {
_scribbleInProgress = true;
return;
} else if (method == 'Scribble.scribbleInteractionFinished') {
_scribbleInProgress = false;
return;
}
// The methods below are only valid when a client exists, i.e. when a field
// is focused.
final ScribbleClient? client = _client;
if (client == null) {
return;
}
final List<dynamic> args = methodCall.arguments as List<dynamic>;
switch (method) {
case 'Scribble.showToolbar':
client.showToolbar();
break;
case 'Scribble.insertTextPlaceholder':
client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble()));
break;
case 'Scribble.removeTextPlaceholder':
client.removeTextPlaceholder();
break;
default:
throw MissingPluginException();
}
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
/// by the engine.
///
/// For example, the registered [ScribbleClient] list is used to respond to
/// UIIndirectScribbleInteraction on an iPad.
static void registerScribbleElement(String elementIdentifier, ScribbleClient scribbleClient) {
_instance._scribbleClients[elementIdentifier] = scribbleClient;
}
/// Unregisters a [ScribbleClient] with [elementIdentifier].
static void unregisterScribbleElement(String elementIdentifier) {
_instance._scribbleClients.remove(elementIdentifier);
}
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
/// Send the bounding boxes of the current selected glyphs in the client to
/// the platform's text input plugin.
///
/// These are used by the engine during a UIDirectScribbleInteraction.
static void setSelectionRects(List<SelectionRect> selectionRects) {
if (!listEquals(_instance._cachedSelectionRects, selectionRects)) {
_instance._cachedSelectionRects = selectionRects;
_instance._channel.invokeMethod<void>(
'Scribble.setSelectionRects',
selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList(),
);
}
}
}
/// An interface to interact with the engine for handwriting text input.
///
/// This is currently only used to handle
/// [UIIndirectScribbleInteraction](https://developer.apple.com/documentation/uikit/uiindirectscribbleinteraction),
/// which is responsible for manually receiving handwritten text input in UIKit.
/// The Flutter engine uses this to receive handwriting input on Flutter text
/// input fields.
mixin ScribbleClient {
/// A unique identifier for this element.
String get elementIdentifier;
/// Called by the engine when the [ScribbleClient] should receive focus.
///
/// For example, this method is called during a UIIndirectScribbleInteraction.
///
/// The [Offset] indicates the location where the focus event happened, which
/// is typically where the cursor should be placed.
void onScribbleFocus(Offset offset);
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds,
/// where the rectangle bounds are in global coordinates.
bool isInScribbleRect(Rect rect);
/// The current bounds of the [ScribbleClient].
Rect get bounds;
/// Requests that the client show the editing toolbar.
///
/// This is used when the platform changes the selection during scribble
/// input.
void showToolbar();
/// Requests that the client add a text placeholder to reserve visual space
/// in the text.
///
/// For example, this is called when responding to UIKit requesting
/// a text placeholder be added at the current selection, such as when
/// requesting additional writing space with iPadOS14 Scribble.
void insertTextPlaceholder(Size size);
/// Requests that the client remove the text placeholder.
void removeTextPlaceholder();
}
/// Represents a selection rect for a character and it's position in the text.
///
/// This is used to report the current text selection rect and position data
/// to the engine for Scribble support on iPadOS 14.
@immutable
class SelectionRect {
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
const SelectionRect({required this.position, required this.bounds});
/// The position of this selection rect within the text String.
final int position;
/// The rectangle representing the bounds of this selection rect within the
/// currently focused [RenderEditable]'s coordinate space.
final Rect bounds;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is SelectionRect
&& other.position == position
&& other.bounds == bounds;
}
@override
int get hashCode => Object.hash(position, bounds);
@override
String toString() => 'SelectionRect($position, $bounds)';
}
......@@ -222,6 +222,38 @@ class SystemChannels {
JSONMethodCodec(),
);
/// A JSON [MethodChannel] for handling handwriting input.
///
/// This method channel is used by iPadOS 14's Scribble feature where writing
/// with an Apple Pencil on top of a text field inserts text into the field.
///
/// The following methods are defined for this channel:
///
/// * `Scribble.focusElement`: Indicates that focus is requested at the given
/// [Offset].
///
/// * `Scribble.requestElementsInRect`: Returns a List of identifiers and
/// bounds for the [ScribbleClient]s that lie within the given Rect.
///
/// * `Scribble.scribbleInteractionBegan`: Indicates that handwriting input
/// has started.
///
/// * `Scribble.scribbleInteractionFinished`: Indicates that handwriting input
/// has ended.
///
/// * `Scribble.showToolbar`: Requests that the toolbar be shown, such as
/// when selection is changed by handwriting.
///
/// * `Scribble.insertTextPlaceholder`: Requests that visual writing space is
/// reserved.
///
/// * `Scribble.removeTextPlaceholder`: Requests that any placeholder writing
/// space is removed.
static const MethodChannel scribble = OptionalMethodChannel(
'flutter/scribble',
JSONMethodCodec(),
);
/// A [MethodChannel] for handling spell check for text input.
///
/// This channel exposes the spell check framework for supported platforms.
......
......@@ -1162,84 +1162,12 @@ mixin TextInputClient {
/// * [TextInputControl.show], a method to show the new input control.
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
/// Requests that the client show the editing toolbar, for example when the
/// platform changes the selection through a non-flutter method such as
/// scribble.
void showToolbar() {}
/// Requests that the client add a text placeholder to reserve visual space
/// in the text.
///
/// For example, this is called when responding to UIKit requesting
/// a text placeholder be added at the current selection, such as when
/// requesting additional writing space with iPadOS14 Scribble.
void insertTextPlaceholder(Size size) {}
/// Requests that the client remove the text placeholder.
void removeTextPlaceholder() {}
/// Performs the specified MacOS-specific selector from the
/// `NSStandardKeyBindingResponding` protocol or user-specified selector
/// from `DefaultKeyBinding.Dict`.
void performSelector(String selectorName) {}
}
/// An interface to receive focus from the engine.
///
/// This is currently only used to handle UIIndirectScribbleInteraction.
abstract class ScribbleClient {
/// A unique identifier for this element.
String get elementIdentifier;
/// Called by the engine when the [ScribbleClient] should receive focus.
///
/// For example, this method is called during a UIIndirectScribbleInteraction.
void onScribbleFocus(Offset offset);
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds.
bool isInScribbleRect(Rect rect);
/// The current bounds of the [ScribbleClient].
Rect get bounds;
}
/// Represents a selection rect for a character and it's position in the text.
///
/// This is used to report the current text selection rect and position data
/// to the engine for Scribble support on iPadOS 14.
@immutable
class SelectionRect {
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
const SelectionRect({required this.position, required this.bounds});
/// The position of this selection rect within the text String.
final int position;
/// The rectangle representing the bounds of this selection rect within the
/// currently focused [RenderEditable]'s coordinate space.
final Rect bounds;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is SelectionRect
&& other.position == position
&& other.bounds == bounds;
}
@override
int get hashCode => Object.hash(position, bounds);
@override
String toString() => 'SelectionRect($position, $bounds)';
}
/// An interface to receive granular information from [TextInput].
///
/// See also:
......@@ -1299,7 +1227,6 @@ class TextInputConnection {
Matrix4? _cachedTransform;
Rect? _cachedRect;
Rect? _cachedCaretRect;
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
static int _nextId = 1;
final int _id;
......@@ -1322,12 +1249,6 @@ class TextInputConnection {
/// Whether this connection is currently interacting with the text input control.
bool get attached => TextInput._instance._currentConnection == this;
/// Whether there is currently a Scribble interaction in progress.
///
/// This is used to make sure selection handles are shown when UIKit changes
/// the selection during a Scribble interaction.
bool get scribbleInProgress => TextInput._instance.scribbleInProgress;
/// Requests that the text input control become visible.
void show() {
assert(attached);
......@@ -1408,17 +1329,6 @@ class TextInputConnection {
TextInput._instance._setCaretRect(validRect);
}
/// Send the bounding boxes of the current selected glyphs in the client to
/// the platform's text input plugin.
///
/// These are used by the engine during a UIDirectScribbleInteraction.
void setSelectionRects(List<SelectionRect> selectionRects) {
if (!listEquals(_cachedSelectionRects, selectionRects)) {
_cachedSelectionRects = selectionRects;
TextInput._instance._setSelectionRects(selectionRects);
}
}
/// Send text styling information.
///
/// This information is used by the Flutter Web Engine to change the style
......@@ -1676,6 +1586,10 @@ class TextInput {
/// Ensure that a [TextInput] instance has been set up so that the platform
/// can handle messages on the text input method channel.
@Deprecated(
'Use Scribble.ensureInitialized instead. '
'This feature was deprecated after v3.1.0-9.0.pre.'
)
static void ensureInitialized() {
_instance; // ignore: unnecessary_statements
}
......@@ -1738,16 +1652,6 @@ class TextInput {
TextInputConnection? _currentConnection;
late TextInputConfiguration _currentConfiguration;
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
bool _scribbleInProgress = false;
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
@visibleForTesting
static Map<String, ScribbleClient> get scribbleClients => TextInput._instance._scribbleClients;
/// Returns true if a scribble interaction is currently happening.
bool get scribbleInProgress => _scribbleInProgress;
Future<dynamic> _loudlyHandleTextInputInvocation(MethodCall call) async {
try {
return await _handleTextInputInvocation(call);
......@@ -1764,33 +1668,8 @@ class TextInput {
rethrow;
}
}
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
if (method == 'TextInputClient.focusElement') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
return;
} else if (method == 'TextInputClient.requestElementsInRect') {
final List<double> args = (methodCall.arguments as List<dynamic>).cast<num>().map<double>((num value) => value.toDouble()).toList();
return _scribbleClients.keys.where((String elementIdentifier) {
final Rect rect = Rect.fromLTWH(args[0], args[1], args[2], args[3]);
if (!(_scribbleClients[elementIdentifier]?.isInScribbleRect(rect) ?? false)) {
return false;
}
final Rect bounds = _scribbleClients[elementIdentifier]?.bounds ?? Rect.zero;
return !(bounds == Rect.zero || bounds.hasNaN || bounds.isInfinite);
}).map((String elementIdentifier) {
final Rect bounds = _scribbleClients[elementIdentifier]!.bounds;
return <dynamic>[elementIdentifier, ...<dynamic>[bounds.left, bounds.top, bounds.width, bounds.height]];
}).toList();
} else if (method == 'TextInputClient.scribbleInteractionBegan') {
_scribbleInProgress = true;
return;
} else if (method == 'TextInputClient.scribbleInteractionFinished') {
_scribbleInProgress = false;
return;
}
if (_currentConnection == null) {
return;
}
......@@ -1894,15 +1773,6 @@ class TextInput {
case 'TextInputClient.showAutocorrectionPromptRect':
_currentConnection!._client.showAutocorrectionPromptRect(args[1] as int, args[2] as int);
break;
case 'TextInputClient.showToolbar':
_currentConnection!._client.showToolbar();
break;
case 'TextInputClient.insertTextPlaceholder':
_currentConnection!._client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble()));
break;
case 'TextInputClient.removeTextPlaceholder':
_currentConnection!._client.removeTextPlaceholder();
break;
default:
throw MissingPluginException();
}
......@@ -1986,12 +1856,6 @@ class TextInput {
}
}
void _setSelectionRects(List<SelectionRect> selectionRects) {
for (final TextInputControl control in _inputControls) {
control.setSelectionRects(selectionRects);
}
}
void _setStyle({
required String? fontFamily,
required double? fontSize,
......@@ -2091,20 +1955,6 @@ class TextInput {
control.finishAutofillContext(shouldSave: shouldSave);
}
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
/// by the engine.
///
/// For example, the registered [ScribbleClient] list is used to respond to
/// UIIndirectScribbleInteraction on an iPad.
static void registerScribbleElement(String elementIdentifier, ScribbleClient scribbleClient) {
TextInput._instance._scribbleClients[elementIdentifier] = scribbleClient;
}
/// Unregisters a [ScribbleClient] with [elementIdentifier].
static void unregisterScribbleElement(String elementIdentifier) {
TextInput._instance._scribbleClients.remove(elementIdentifier);
}
}
/// An interface for implementing text input controls that receive text editing
......@@ -2188,12 +2038,6 @@ mixin TextInputControl {
/// changes.
void setCaretRect(Rect rect) {}
/// Informs the text input control about selection area changes.
///
/// This method is called when the attached input client's selection area
/// changes.
void setSelectionRects(List<SelectionRect> selectionRects) {}
/// Informs the text input control about text style changes.
///
/// This method is called on the when the attached input client's text style
......@@ -2316,17 +2160,6 @@ class _PlatformTextInputControl with TextInputControl {
);
}
@override
void setSelectionRects(List<SelectionRect> selectionRects) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList(),
);
}
@override
void setStyle({
required String? fontFamily,
......
......@@ -2560,7 +2560,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (value.text == _value.text && value.composing == _value.composing) {
// `selection` is the only change.
_handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard);
_handleSelectionChanged(
value.selection,
Scribble.scribbleInProgress
? SelectionChangedCause.scribble
: SelectionChangedCause.keyboard,
);
} else {
// Only hide the toolbar overlay, the selection handle's visibility will be handled
// by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673
......@@ -3519,7 +3524,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
graphemeStart = graphemeEnd;
}
_textInputConnection!.setSelectionRects(rects);
Scribble.setSelectionRects(rects);
}
void _updateSizeAndTransform() {
......@@ -3530,7 +3535,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_updateSelectionRects();
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
} else if (_placeholderLocation != -1) {
removeTextPlaceholder();
_removeTextPlaceholder();
}
}
......@@ -3623,7 +3628,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
///
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// is already shown, or when no text selection currently exists.
@override
bool showToolbar() {
// Web is using native dom elements to enable clipboard functionality of the
// toolbar: copy, paste, select, cut. It might also provide additional
......@@ -3694,39 +3698,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
// text.
//
// A value of -1 indicates there should be no placeholder, otherwise the
// value should be between 0 and the length of the text, inclusive.
int _placeholderLocation = -1;
@override
void insertTextPlaceholder(Size size) {
if (!widget.scribbleEnabled) {
return;
}
if (!widget.controller.selection.isValid) {
return;
}
setState(() {
_placeholderLocation = _value.text.length - widget.controller.selection.end;
});
}
@override
void removeTextPlaceholder() {
if (!widget.scribbleEnabled) {
return;
}
setState(() {
_placeholderLocation = -1;
});
}
@override
void performSelector(String selectorName) {
final Intent? intent = intentForMacOSSelector(selectorName);
......@@ -4099,6 +4070,35 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return Actions.invoke(context, intent);
}
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
// text.
//
// A value of -1 indicates there should be no placeholder, otherwise the
// value should be between 0 and the length of the text, inclusive.
int _placeholderLocation = -1;
void _onPlaceholderLocationChanged(int location) {
setState(() {
_placeholderLocation = location;
});
}
void _onScribbleFocus(Offset offset) {
widget.focusNode.requestFocus();
renderEditable.selectPositionAt(from: offset, cause: SelectionChangedCause.scribble);
_openInputConnection();
_updateSelectionRects(force: true);
}
void _removeTextPlaceholder() {
if (!widget.scribbleEnabled) {
return;
}
setState(() {
_placeholderLocation = -1;
});
}
/// The default behavior used if [onTapOutside] is null.
///
......@@ -4216,12 +4216,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onPaste: _semanticsOnPaste(controls),
child: _ScribbleFocusable(
focusNode: widget.focusNode,
editableKey: _editableKey,
enabled: widget.scribbleEnabled,
updateSelectionRects: () {
_openInputConnection();
_updateSelectionRects(force: true);
},
onPlaceholderLocationChanged: _onPlaceholderLocationChanged,
onScribbleFocus: _onScribbleFocus,
onShowToolbar: showToolbar,
readOnly: widget.readOnly,
value: _value,
child: _Editable(
key: _editableKey,
startHandleLayerLink: _startHandleLayerLink,
......@@ -4536,6 +4536,13 @@ class _Editable extends MultiChildRenderObjectWidget {
}
}
/// A function that that takes a placeholder location as an int offset into some
/// text.
typedef _PlaceholderLocationCallback = void Function(int location);
/// A function that takes the Offset at which focus is requested.
typedef _ScribbleFocusCallback = void Function(Offset offset);
@immutable
class _ScribbleCacheKey {
const _ScribbleCacheKey({
......@@ -4576,55 +4583,88 @@ class _ScribbleCacheKey {
}
}
/// A widget that provides the ability to receive handwriting input from
/// [Scribble].
class _ScribbleFocusable extends StatefulWidget {
const _ScribbleFocusable({
required this.child,
required this.focusNode,
required this.editableKey,
required this.updateSelectionRects,
required this.enabled,
required this.focusNode,
required this.onPlaceholderLocationChanged,
required this.onScribbleFocus,
required this.onShowToolbar,
required this.readOnly,
required this.value,
});
final Widget child;
final FocusNode focusNode;
final GlobalKey editableKey;
final VoidCallback updateSelectionRects;
final bool enabled;
final FocusNode focusNode;
final _PlaceholderLocationCallback onPlaceholderLocationChanged;
final _ScribbleFocusCallback onScribbleFocus;
final VoidCallback onShowToolbar;
final bool readOnly;
final TextEditingValue value;
@override
_ScribbleFocusableState createState() => _ScribbleFocusableState();
}
class _ScribbleFocusableState extends State<_ScribbleFocusable> implements ScribbleClient {
class _ScribbleFocusableState extends State<_ScribbleFocusable> with ScribbleClient {
_ScribbleFocusableState(): _elementIdentifier = (_nextElementIdentifier++).toString();
void _onFocusChange() {
_updateClient(widget.focusNode.hasFocus);
}
void _updateClient(bool hasFocus) {
if (hasFocus) {
if (Scribble.client != this) {
Scribble.client = this;
}
} else if (Scribble.client == this) {
Scribble.client = null;
}
}
@override
void initState() {
super.initState();
_updateClient(widget.focusNode.hasFocus);
widget.focusNode.addListener(_onFocusChange);
if (widget.enabled) {
TextInput.registerScribbleElement(elementIdentifier, this);
Scribble.registerScribbleElement(elementIdentifier, this);
}
}
@override
void didUpdateWidget(_ScribbleFocusable oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.focusNode != widget.focusNode) {
oldWidget.focusNode.removeListener(_onFocusChange);
widget.focusNode.addListener(_onFocusChange);
_updateClient(widget.focusNode.hasFocus);
}
if (!oldWidget.enabled && widget.enabled) {
TextInput.registerScribbleElement(elementIdentifier, this);
Scribble.registerScribbleElement(elementIdentifier, this);
}
if (oldWidget.enabled && !widget.enabled) {
TextInput.unregisterScribbleElement(elementIdentifier);
Scribble.unregisterScribbleElement(elementIdentifier);
}
}
@override
void dispose() {
TextInput.unregisterScribbleElement(elementIdentifier);
Scribble.unregisterScribbleElement(elementIdentifier);
widget.focusNode.removeListener(_onFocusChange);
if (Scribble.client == this) {
Scribble.client = null;
}
super.dispose();
}
RenderEditable? get renderEditable => widget.editableKey.currentContext?.findRenderObject() as RenderEditable?;
// Start ScribbleClient.
static int _nextElementIdentifier = 1;
final String _elementIdentifier;
......@@ -4634,15 +4674,38 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> implements Scrib
@override
void onScribbleFocus(Offset offset) {
widget.focusNode.requestFocus();
renderEditable?.selectPositionAt(from: offset, cause: SelectionChangedCause.scribble);
widget.updateSelectionRects();
return widget.onScribbleFocus(offset);
}
@override
void insertTextPlaceholder(Size size) {
if (!widget.enabled || !widget.value.selection.isValid || widget.readOnly) {
return;
}
widget.onPlaceholderLocationChanged(
widget.value.text.length - widget.value.selection.end,
);
}
@override
void removeTextPlaceholder() {
if (!widget.enabled) {
return;
}
widget.onPlaceholderLocationChanged(-1);
}
@override
void showToolbar() {
widget.onShowToolbar();
}
@override
bool isInScribbleRect(Rect rect) {
final Rect calculatedBounds = bounds;
if (renderEditable?.readOnly ?? false) {
if (widget.readOnly) {
return false;
}
if (calculatedBounds == Rect.zero) {
......@@ -4654,7 +4717,8 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> implements Scrib
final Rect intersection = calculatedBounds.intersect(rect);
final HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, intersection.center);
return result.path.any((HitTestEntry entry) => entry.target == renderEditable);
final RenderObject? renderObject = context.findRenderObject();
return result.path.any((HitTestEntry entry) => entry.target == renderObject);
}
@override
......@@ -4667,6 +4731,8 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> implements Scrib
return MatrixUtils.transformRect(transform, Rect.fromLTWH(0, 0, box.size.width, box.size.height));
}
// End ScribbleClient.
@override
Widget build(BuildContext context) {
return widget.child;
......
......@@ -147,21 +147,6 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
@override
void autofill(TextEditingValue newEditingValue) => updateEditingValue(newEditingValue);
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
@override
void performSelector(String selectorName) {
latestMethodCall = 'performSelector';
......
......@@ -106,11 +106,4 @@ void main() {
await rootBundle.loadString('test_asset2');
expect(flutterAssetsCallCount, 4);
});
test('initInstances sets a default method call handler for SystemChannels.textInput', () async {
final ByteData message = const JSONMessageCodec().encodeMessage(<String, dynamic>{'method': 'TextInput.requestElementsInRect', 'args': null})!;
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/textinput', message, (ByteData? data) {
expect(data, isNotNull);
});
});
}
......@@ -271,21 +271,6 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
latestMethodCall = 'showAutocorrectionPromptRect';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
@override
void performSelector(String selectorName) {
latestMethodCall = 'performSelector';
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'text_input_utils.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('ScribbleClient showToolbar method is called', () async {
final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target');
Scribble.client = targetElement;
expect(targetElement.latestMethodCall, isEmpty);
// Send showToolbar message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'Scribble.showToolbar',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? _) {},
);
expect(targetElement.latestMethodCall, 'showToolbar');
});
test('ScribbleClient removeTextPlaceholder method is called', () async {
final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target');
Scribble.client = targetElement;
expect(targetElement.latestMethodCall, isEmpty);
// Send removeTextPlaceholder message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'Scribble.removeTextPlaceholder',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? _) {},
);
expect(targetElement.latestMethodCall, 'removeTextPlaceholder');
});
test('ScribbleClient insertTextPlaceholder method is called', () async {
final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target');
Scribble.client = targetElement;
expect(targetElement.latestMethodCall, isEmpty);
// Send insertTextPlaceholder message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'Scribble.insertTextPlaceholder',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? _) {},
);
expect(targetElement.latestMethodCall, 'insertTextPlaceholder');
});
test('ScribbleClient scribbleInteractionBegan and scribbleInteractionFinished', () async {
Scribble.ensureInitialized();
expect(Scribble.scribbleInProgress, isFalse);
// Send scribbleInteractionBegan message.
ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'Scribble.scribbleInteractionBegan',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? _) {},
);
expect(Scribble.scribbleInProgress, isTrue);
// Send scribbleInteractionFinished message.
messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'Scribble.scribbleInteractionFinished',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? _) {},
);
expect(Scribble.scribbleInProgress, isFalse);
});
test('ScribbleClient focusElement', () async {
final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target');
Scribble.registerScribbleElement(targetElement.elementIdentifier, targetElement);
final FakeScribbleElement otherElement = FakeScribbleElement(elementIdentifier: 'other');
Scribble.registerScribbleElement(otherElement.elementIdentifier, otherElement);
expect(targetElement.latestMethodCall, isEmpty);
expect(otherElement.latestMethodCall, isEmpty);
// Send focusElement message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[targetElement.elementIdentifier, 0.0, 0.0],
'method': 'Scribble.focusElement',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? _) {},
);
Scribble.unregisterScribbleElement(targetElement.elementIdentifier);
Scribble.unregisterScribbleElement(otherElement.elementIdentifier);
expect(targetElement.latestMethodCall, 'onScribbleFocus');
expect(otherElement.latestMethodCall, isEmpty);
});
test('ScribbleClient requestElementsInRect', () async {
final List<FakeScribbleElement> targetElements = <FakeScribbleElement>[
FakeScribbleElement(elementIdentifier: 'target1', bounds: const Rect.fromLTWH(0.0, 0.0, 100.0, 100.0)),
FakeScribbleElement(elementIdentifier: 'target2', bounds: const Rect.fromLTWH(0.0, 100.0, 100.0, 100.0)),
];
final List<FakeScribbleElement> otherElements = <FakeScribbleElement>[
FakeScribbleElement(elementIdentifier: 'other1', bounds: const Rect.fromLTWH(100.0, 0.0, 100.0, 100.0)),
FakeScribbleElement(elementIdentifier: 'other2', bounds: const Rect.fromLTWH(100.0, 100.0, 100.0, 100.0)),
];
void registerElements(FakeScribbleElement element) => Scribble.registerScribbleElement(element.elementIdentifier, element);
void unregisterElements(FakeScribbleElement element) => Scribble.unregisterScribbleElement(element.elementIdentifier);
<FakeScribbleElement>[...targetElements, ...otherElements].forEach(registerElements);
// Send requestElementsInRect message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[0.0, 50.0, 50.0, 100.0],
'method': 'Scribble.requestElementsInRect',
});
ByteData? responseBytes;
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/scribble',
messageBytes,
(ByteData? response) {
responseBytes = response;
},
);
<FakeScribbleElement>[...targetElements, ...otherElements].forEach(unregisterElements);
final List<List<dynamic>> responses = (const JSONMessageCodec().decodeMessage(responseBytes) as List<dynamic>).cast<List<dynamic>>();
expect(responses.first.length, 2);
expect(responses.first.first, containsAllInOrder(<dynamic>[targetElements.first.elementIdentifier, 0.0, 0.0, 100.0, 100.0]));
expect(responses.first.last, containsAllInOrder(<dynamic>[targetElements.last.elementIdentifier, 0.0, 100.0, 100.0, 100.0]));
});
}
class FakeScribbleClient implements ScribbleClient {
FakeScribbleClient();
String latestMethodCall = '';
@override
String get elementIdentifier => '';
@override
void onScribbleFocus(Offset offset) {
latestMethodCall = 'onScribbleFocus';
}
@override
bool isInScribbleRect(Rect rect) {
latestMethodCall = 'isInScribbleRect';
return false;
}
@override
Rect get bounds => Rect.zero;
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
}
......@@ -610,148 +610,6 @@ void main() {
expect(client.latestMethodCall, 'showAutocorrectionPromptRect');
});
test('TextInputClient showToolbar method is called', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
expect(client.latestMethodCall, isEmpty);
// Send showToolbar message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'TextInputClient.showToolbar',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(client.latestMethodCall, 'showToolbar');
});
});
group('Scribble interactions', () {
tearDown(() {
TextInputConnection.debugResetId();
});
test('TextInputClient scribbleInteractionBegan and scribbleInteractionFinished', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
final TextInputConnection connection = TextInput.attach(client, configuration);
expect(connection.scribbleInProgress, false);
// Send scribbleInteractionBegan message.
ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'TextInputClient.scribbleInteractionBegan',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(connection.scribbleInProgress, true);
// Send scribbleInteractionFinished message.
messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[1, 0, 1],
'method': 'TextInputClient.scribbleInteractionFinished',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
expect(connection.scribbleInProgress, false);
});
test('TextInputClient focusElement', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
final FakeScribbleElement targetElement = FakeScribbleElement(elementIdentifier: 'target');
TextInput.registerScribbleElement(targetElement.elementIdentifier, targetElement);
final FakeScribbleElement otherElement = FakeScribbleElement(elementIdentifier: 'other');
TextInput.registerScribbleElement(otherElement.elementIdentifier, otherElement);
expect(targetElement.latestMethodCall, isEmpty);
expect(otherElement.latestMethodCall, isEmpty);
// Send focusElement message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[targetElement.elementIdentifier, 0.0, 0.0],
'method': 'TextInputClient.focusElement',
});
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? _) {},
);
TextInput.unregisterScribbleElement(targetElement.elementIdentifier);
TextInput.unregisterScribbleElement(otherElement.elementIdentifier);
expect(targetElement.latestMethodCall, 'onScribbleFocus');
expect(otherElement.latestMethodCall, isEmpty);
});
test('TextInputClient requestElementsInRect', () async {
// Assemble a TextInputConnection so we can verify its change in state.
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
const TextInputConfiguration configuration = TextInputConfiguration();
TextInput.attach(client, configuration);
final List<FakeScribbleElement> targetElements = <FakeScribbleElement>[
FakeScribbleElement(elementIdentifier: 'target1', bounds: const Rect.fromLTWH(0.0, 0.0, 100.0, 100.0)),
FakeScribbleElement(elementIdentifier: 'target2', bounds: const Rect.fromLTWH(0.0, 100.0, 100.0, 100.0)),
];
final List<FakeScribbleElement> otherElements = <FakeScribbleElement>[
FakeScribbleElement(elementIdentifier: 'other1', bounds: const Rect.fromLTWH(100.0, 0.0, 100.0, 100.0)),
FakeScribbleElement(elementIdentifier: 'other2', bounds: const Rect.fromLTWH(100.0, 100.0, 100.0, 100.0)),
];
void registerElements(FakeScribbleElement element) => TextInput.registerScribbleElement(element.elementIdentifier, element);
void unregisterElements(FakeScribbleElement element) => TextInput.unregisterScribbleElement(element.elementIdentifier);
<FakeScribbleElement>[...targetElements, ...otherElements].forEach(registerElements);
// Send requestElementsInRect message.
final ByteData? messageBytes =
const JSONMessageCodec().encodeMessage(<String, dynamic>{
'args': <dynamic>[0.0, 50.0, 50.0, 100.0],
'method': 'TextInputClient.requestElementsInRect',
});
ByteData? responseBytes;
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/textinput',
messageBytes,
(ByteData? response) {
responseBytes = response;
},
);
<FakeScribbleElement>[...targetElements, ...otherElements].forEach(unregisterElements);
final List<List<dynamic>> responses = (const JSONMessageCodec().decodeMessage(responseBytes) as List<dynamic>).cast<List<dynamic>>();
expect(responses.first.length, 2);
expect(responses.first.first, containsAllInOrder(<dynamic>[targetElements.first.elementIdentifier, 0.0, 0.0, 100.0, 100.0]));
expect(responses.first.last, containsAllInOrder(<dynamic>[targetElements.last.elementIdentifier, 0.0, 100.0, 100.0, 100.0]));
});
});
test('TextEditingValue.isComposingRangeValid', () async {
......@@ -906,12 +764,6 @@ void main() {
expect(fakeTextChannel.outgoingCalls.length, 6);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setEditableSizeAndTransform');
connection.setSelectionRects(const <SelectionRect>[SelectionRect(position: 0, bounds: Rect.zero)]);
expectedMethodCalls.add('setSelectionRects');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 7);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setSelectionRects');
connection.setStyle(
fontFamily: null,
fontSize: null,
......@@ -921,20 +773,20 @@ void main() {
);
expectedMethodCalls.add('setStyle');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 8);
expect(fakeTextChannel.outgoingCalls.length, 7);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setStyle');
connection.close();
expectedMethodCalls.add('detach');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 9);
expect(fakeTextChannel.outgoingCalls.length, 8);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.clearClient');
expectedMethodCalls.add('hide');
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAsync(() async {});
await expectLater(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 10);
expect(fakeTextChannel.outgoingCalls.length, 9);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.hide');
});
......@@ -998,11 +850,6 @@ class FakeTextInputClient with TextInputClient {
latestMethodCall = 'showAutocorrectionPromptRect';
}
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
TextInputConfiguration get configuration => const TextInputConfiguration();
@override
......@@ -1010,16 +857,6 @@ class FakeTextInputClient with TextInputClient {
latestMethodCall = 'didChangeInputControl';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
@override
void performSelector(String selectorName) {
latestMethodCall = 'performSelector';
......@@ -1078,11 +915,6 @@ class FakeTextInputControl with TextInputControl {
methodCalls.add('setEditableSizeAndTransform');
}
@override
void setSelectionRects(List<SelectionRect> selectionRects) {
methodCalls.add('setSelectionRects');
}
@override
void setStyle({
required String? fontFamily,
......
......@@ -65,7 +65,7 @@ class FakeTextChannel implements MethodChannel {
}
}
class FakeScribbleElement implements ScribbleClient {
class FakeScribbleElement with ScribbleClient {
FakeScribbleElement({required String elementIdentifier, Rect bounds = Rect.zero})
: _elementIdentifier = elementIdentifier,
_bounds = bounds;
......@@ -89,4 +89,19 @@ class FakeScribbleElement implements ScribbleClient {
void onScribbleFocus(Offset offset) {
latestMethodCall = 'onScribbleFocus';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
}
@override
void removeTextPlaceholder() {
latestMethodCall = 'removeTextPlaceholder';
}
@override
void showToolbar() {
latestMethodCall = 'showToolbar';
}
}
......@@ -2210,6 +2210,7 @@ void main() {
final TextEditingController controller =
TextEditingController(text: 'Lorem ipsum dolor sit amet');
late SelectionChangedCause selectionCause;
Scribble.ensureInitialized();
await tester.pumpWidget(
MaterialApp(
......@@ -2229,7 +2230,7 @@ void main() {
),
);
await tester.testTextInput.scribbleFocusElement(TextInput.scribbleClients.keys.first, Offset.zero);
await tester.testTextInput.scribbleFocusElement(Scribble.scribbleClients.keys.first, Offset.zero);
expect(focusNode.hasFocus, true);
expect(selectionCause, SelectionChangedCause.scribble);
......@@ -2255,7 +2256,7 @@ void main() {
),
);
final List<dynamic> elementEntry = <dynamic>[TextInput.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0];
final List<dynamic> elementEntry = <dynamic>[Scribble.scribbleClients.keys.first, 0.0, 0.0, 800.0, 600.0];
List<List<dynamic>> elements = await tester.testTextInput.scribbleRequestElementsInRect(const Rect.fromLTWH(0, 0, 1, 1));
expect(elements.first, containsAll(elementEntry));
......@@ -4629,8 +4630,8 @@ void main() {
tester.binding.window.physicalSizeTestValue = const Size(750.0, 1334.0);
final List<List<SelectionRect>> log = <List<SelectionRect>>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'TextInput.setSelectionRects') {
SystemChannels.scribble.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Scribble.setSelectionRects') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
final List<SelectionRect> selectionRects = <SelectionRect>[];
for (final dynamic rect in args) {
......@@ -4801,6 +4802,76 @@ void main() {
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); // [intended]
testWidgets('scribble client is set based on most recent focus', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final TextEditingController controller = TextEditingController();
controller.text = 'Text1';
final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey();
final FocusNode focusNode1 = FocusNode();
final FocusNode focusNode2 = FocusNode();
Scribble.client = null;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
EditableText(
key: key1,
controller: TextEditingController(),
focusNode: focusNode1,
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
scribbleEnabled: false,
),
EditableText(
key: key2,
controller: TextEditingController(),
focusNode: focusNode2,
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
scribbleEnabled: false,
),
],
),
),
),
);
expect(Scribble.client, isNull);
focusNode1.requestFocus();
await tester.pump();
expect(Scribble.client, isNotNull);
final ScribbleClient client1 = Scribble.client!;
focusNode2.requestFocus();
await tester.pump();
expect(Scribble.client, isNot(client1));
expect(Scribble.client, isNotNull);
focusNode2.unfocus();
await tester.pump();
expect(Scribble.client, isNull);
// On web, we should rely on the browser's implementation of Scribble.
}, skip: kIsWeb); // [intended]
testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {
......
......@@ -285,10 +285,10 @@ class TestTextInput {
Future<void> startScribbleInteraction() async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
SystemChannels.scribble.name,
SystemChannels.scribble.codec.encodeMethodCall(
MethodCall(
'TextInputClient.scribbleInteractionBegan',
'Scribble.scribbleInteractionBegan',
<dynamic>[_client ?? -1,]
),
),
......@@ -300,10 +300,10 @@ class TestTextInput {
Future<void> scribbleFocusElement(String elementIdentifier, Offset offset) async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
SystemChannels.scribble.name,
SystemChannels.scribble.codec.encodeMethodCall(
MethodCall(
'TextInputClient.focusElement',
'Scribble.focusElement',
<dynamic>[elementIdentifier, offset.dx, offset.dy]
),
),
......@@ -316,15 +316,15 @@ class TestTextInput {
assert(isRegistered);
List<List<dynamic>> response = <List<dynamic>>[];
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
SystemChannels.scribble.name,
SystemChannels.scribble.codec.encodeMethodCall(
MethodCall(
'TextInputClient.requestElementsInRect',
'Scribble.requestElementsInRect',
<dynamic>[rect.left, rect.top, rect.width, rect.height]
),
),
(ByteData? data) {
response = (SystemChannels.textInput.codec.decodeEnvelope(data!) as List<dynamic>).map((dynamic element) => element as List<dynamic>).toList();
response = (SystemChannels.scribble.codec.decodeEnvelope(data!) as List<dynamic>).map((dynamic element) => element as List<dynamic>).toList();
},
);
......@@ -335,10 +335,10 @@ class TestTextInput {
Future<void> scribbleInsertPlaceholder() async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
SystemChannels.scribble.name,
SystemChannels.scribble.codec.encodeMethodCall(
MethodCall(
'TextInputClient.insertTextPlaceholder',
'Scribble.insertTextPlaceholder',
<dynamic>[_client ?? -1, 0.0, 0.0]
),
),
......@@ -350,10 +350,10 @@ class TestTextInput {
Future<void> scribbleRemovePlaceholder() async {
assert(isRegistered);
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
SystemChannels.scribble.name,
SystemChannels.scribble.codec.encodeMethodCall(
MethodCall(
'TextInputClient.removeTextPlaceholder',
'Scribble.removeTextPlaceholder',
<dynamic>[_client ?? -1]
),
),
......
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