Skip to content

Commit b571abf

Browse files
authored
Scribble mixin (#104128)
Refactors methods related to the iPad Scribble feature out of TextInputClient
1 parent b4058b9 commit b571abf

14 files changed

+730
-462
lines changed

packages/flutter/lib/services.dart

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export 'src/services/raw_keyboard_macos.dart';
3737
export 'src/services/raw_keyboard_web.dart';
3838
export 'src/services/raw_keyboard_windows.dart';
3939
export 'src/services/restoration.dart';
40+
export 'src/services/scribble.dart';
4041
export 'src/services/service_extensions.dart';
4142
export 'src/services/spell_check.dart';
4243
export 'src/services/system_channels.dart';

packages/flutter/lib/src/services/binding.dart

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import 'binary_messenger.dart';
1515
import 'hardware_keyboard.dart';
1616
import 'message_codec.dart';
1717
import 'restoration.dart';
18+
import 'scribble.dart';
1819
import 'service_extensions.dart';
1920
import 'system_channels.dart';
20-
import 'text_input.dart';
2121

2222
export 'dart:ui' show ChannelBuffers, RootIsolateToken;
2323

@@ -43,7 +43,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
4343
SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));
4444
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
4545
SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage);
46-
TextInput.ensureInitialized();
46+
Scribble.ensureInitialized();
4747
readInitialLifecycleStateFromNativeWindow();
4848
}
4949

@@ -326,7 +326,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
326326
void setSystemUiChangeCallback(SystemUiChangeCallback? callback) {
327327
_systemUiChangeCallback = callback;
328328
}
329-
330329
}
331330

332331
/// Signature for listening to changes in the [SystemUiMode].
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ui';
6+
7+
import 'package:flutter/foundation.dart';
8+
9+
import 'message_codec.dart';
10+
import 'platform_channel.dart';
11+
import 'system_channels.dart';
12+
13+
/// An interface into system-level handwriting text input.
14+
///
15+
/// This is typically used by implemeting the methods in [ScribbleClient] in a
16+
/// class, usually a [State], and setting an instance of it to [client]. The
17+
/// relevant methods on [ScribbleClient] will be called in response to method
18+
/// channel calls on [SystemChannels.scribble].
19+
///
20+
/// Currently, handwriting input is supported in the iOS embedder with the Apple
21+
/// Pencil.
22+
///
23+
/// [EditableText] uses this class via [ScribbleClient] to automatically support
24+
/// handwriting input when [EditableText.scribbleEnabled] is set to true.
25+
///
26+
/// See also:
27+
///
28+
/// * [SystemChannels.scribble], which is the [MethodChannel] used by this
29+
/// class, and which has a list of the methods that this class handles.
30+
class Scribble {
31+
Scribble._() {
32+
_channel.setMethodCallHandler(_handleScribbleInvocation);
33+
}
34+
35+
/// Ensure that a [Scribble] instance has been set up so that the platform
36+
/// can handle messages on the scribble method channel.
37+
static void ensureInitialized() {
38+
_instance; // ignore: unnecessary_statements
39+
}
40+
41+
/// Set the [MethodChannel] used to communicate with the system's text input
42+
/// control.
43+
///
44+
/// This is only meant for testing within the Flutter SDK. Changing this
45+
/// will break the ability to do handwriting input. This has no effect if
46+
/// asserts are disabled.
47+
@visibleForTesting
48+
static void setChannel(MethodChannel newChannel) {
49+
assert(() {
50+
_instance._channel = newChannel..setMethodCallHandler(_instance._handleScribbleInvocation);
51+
return true;
52+
}());
53+
}
54+
55+
static final Scribble _instance = Scribble._();
56+
57+
/// Set the given [ScribbleClient] as the single active client.
58+
///
59+
/// This is usually based on the [ScribbleClient] receiving focus.
60+
static set client(ScribbleClient? client) {
61+
_instance._client = client;
62+
}
63+
64+
/// Return the current active [ScribbleClient], or null if none.
65+
static ScribbleClient? get client => _instance._client;
66+
67+
ScribbleClient? _client;
68+
69+
MethodChannel _channel = SystemChannels.scribble;
70+
71+
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
72+
bool _scribbleInProgress = false;
73+
74+
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
75+
@visibleForTesting
76+
static Map<String, ScribbleClient> get scribbleClients => Scribble._instance._scribbleClients;
77+
78+
/// Returns true if a scribble interaction is currently happening.
79+
static bool get scribbleInProgress => _instance._scribbleInProgress;
80+
81+
Future<dynamic> _handleScribbleInvocation(MethodCall methodCall) async {
82+
final String method = methodCall.method;
83+
if (method == 'Scribble.focusElement') {
84+
final List<dynamic> args = methodCall.arguments as List<dynamic>;
85+
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
86+
return;
87+
} else if (method == 'Scribble.requestElementsInRect') {
88+
final List<double> args = (methodCall.arguments as List<dynamic>).cast<num>().map<double>((num value) => value.toDouble()).toList();
89+
return _scribbleClients.keys.where((String elementIdentifier) {
90+
final Rect rect = Rect.fromLTWH(args[0], args[1], args[2], args[3]);
91+
if (!(_scribbleClients[elementIdentifier]?.isInScribbleRect(rect) ?? false)) {
92+
return false;
93+
}
94+
final Rect bounds = _scribbleClients[elementIdentifier]?.bounds ?? Rect.zero;
95+
return !(bounds == Rect.zero || bounds.hasNaN || bounds.isInfinite);
96+
}).map((String elementIdentifier) {
97+
final Rect bounds = _scribbleClients[elementIdentifier]!.bounds;
98+
return <dynamic>[elementIdentifier, ...<dynamic>[bounds.left, bounds.top, bounds.width, bounds.height]];
99+
}).toList();
100+
} else if (method == 'Scribble.scribbleInteractionBegan') {
101+
_scribbleInProgress = true;
102+
return;
103+
} else if (method == 'Scribble.scribbleInteractionFinished') {
104+
_scribbleInProgress = false;
105+
return;
106+
}
107+
108+
// The methods below are only valid when a client exists, i.e. when a field
109+
// is focused.
110+
final ScribbleClient? client = _client;
111+
if (client == null) {
112+
return;
113+
}
114+
115+
final List<dynamic> args = methodCall.arguments as List<dynamic>;
116+
switch (method) {
117+
case 'Scribble.showToolbar':
118+
client.showToolbar();
119+
break;
120+
case 'Scribble.insertTextPlaceholder':
121+
client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble()));
122+
break;
123+
case 'Scribble.removeTextPlaceholder':
124+
client.removeTextPlaceholder();
125+
break;
126+
default:
127+
throw MissingPluginException();
128+
}
129+
}
130+
131+
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
132+
/// by the engine.
133+
///
134+
/// For example, the registered [ScribbleClient] list is used to respond to
135+
/// UIIndirectScribbleInteraction on an iPad.
136+
static void registerScribbleElement(String elementIdentifier, ScribbleClient scribbleClient) {
137+
_instance._scribbleClients[elementIdentifier] = scribbleClient;
138+
}
139+
140+
/// Unregisters a [ScribbleClient] with [elementIdentifier].
141+
static void unregisterScribbleElement(String elementIdentifier) {
142+
_instance._scribbleClients.remove(elementIdentifier);
143+
}
144+
145+
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
146+
147+
/// Send the bounding boxes of the current selected glyphs in the client to
148+
/// the platform's text input plugin.
149+
///
150+
/// These are used by the engine during a UIDirectScribbleInteraction.
151+
static void setSelectionRects(List<SelectionRect> selectionRects) {
152+
if (!listEquals(_instance._cachedSelectionRects, selectionRects)) {
153+
_instance._cachedSelectionRects = selectionRects;
154+
_instance._channel.invokeMethod<void>(
155+
'Scribble.setSelectionRects',
156+
selectionRects.map((SelectionRect rect) {
157+
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
158+
}).toList(),
159+
);
160+
}
161+
}
162+
}
163+
164+
/// An interface to interact with the engine for handwriting text input.
165+
///
166+
/// This is currently only used to handle
167+
/// [UIIndirectScribbleInteraction](https://developer.apple.com/documentation/uikit/uiindirectscribbleinteraction),
168+
/// which is responsible for manually receiving handwritten text input in UIKit.
169+
/// The Flutter engine uses this to receive handwriting input on Flutter text
170+
/// input fields.
171+
mixin ScribbleClient {
172+
/// A unique identifier for this element.
173+
String get elementIdentifier;
174+
175+
/// Called by the engine when the [ScribbleClient] should receive focus.
176+
///
177+
/// For example, this method is called during a UIIndirectScribbleInteraction.
178+
///
179+
/// The [Offset] indicates the location where the focus event happened, which
180+
/// is typically where the cursor should be placed.
181+
void onScribbleFocus(Offset offset);
182+
183+
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds,
184+
/// where the rectangle bounds are in global coordinates.
185+
bool isInScribbleRect(Rect rect);
186+
187+
/// The current bounds of the [ScribbleClient].
188+
Rect get bounds;
189+
190+
/// Requests that the client show the editing toolbar.
191+
///
192+
/// This is used when the platform changes the selection during scribble
193+
/// input.
194+
void showToolbar();
195+
196+
/// Requests that the client add a text placeholder to reserve visual space
197+
/// in the text.
198+
///
199+
/// For example, this is called when responding to UIKit requesting
200+
/// a text placeholder be added at the current selection, such as when
201+
/// requesting additional writing space with iPadOS14 Scribble.
202+
void insertTextPlaceholder(Size size);
203+
204+
/// Requests that the client remove the text placeholder.
205+
void removeTextPlaceholder();
206+
}
207+
208+
/// Represents a selection rect for a character and it's position in the text.
209+
///
210+
/// This is used to report the current text selection rect and position data
211+
/// to the engine for Scribble support on iPadOS 14.
212+
@immutable
213+
class SelectionRect {
214+
/// Constructor for creating a [SelectionRect] from a text [position] and
215+
/// [bounds].
216+
const SelectionRect({required this.position, required this.bounds});
217+
218+
/// The position of this selection rect within the text String.
219+
final int position;
220+
221+
/// The rectangle representing the bounds of this selection rect within the
222+
/// currently focused [RenderEditable]'s coordinate space.
223+
final Rect bounds;
224+
225+
@override
226+
bool operator ==(Object other) {
227+
if (identical(this, other)) {
228+
return true;
229+
}
230+
if (runtimeType != other.runtimeType) {
231+
return false;
232+
}
233+
return other is SelectionRect
234+
&& other.position == position
235+
&& other.bounds == bounds;
236+
}
237+
238+
@override
239+
int get hashCode => Object.hash(position, bounds);
240+
241+
@override
242+
String toString() => 'SelectionRect($position, $bounds)';
243+
}

packages/flutter/lib/src/services/system_channels.dart

+32
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,38 @@ class SystemChannels {
222222
JSONMethodCodec(),
223223
);
224224

225+
/// A JSON [MethodChannel] for handling handwriting input.
226+
///
227+
/// This method channel is used by iPadOS 14's Scribble feature where writing
228+
/// with an Apple Pencil on top of a text field inserts text into the field.
229+
///
230+
/// The following methods are defined for this channel:
231+
///
232+
/// * `Scribble.focusElement`: Indicates that focus is requested at the given
233+
/// [Offset].
234+
///
235+
/// * `Scribble.requestElementsInRect`: Returns a List of identifiers and
236+
/// bounds for the [ScribbleClient]s that lie within the given Rect.
237+
///
238+
/// * `Scribble.scribbleInteractionBegan`: Indicates that handwriting input
239+
/// has started.
240+
///
241+
/// * `Scribble.scribbleInteractionFinished`: Indicates that handwriting input
242+
/// has ended.
243+
///
244+
/// * `Scribble.showToolbar`: Requests that the toolbar be shown, such as
245+
/// when selection is changed by handwriting.
246+
///
247+
/// * `Scribble.insertTextPlaceholder`: Requests that visual writing space is
248+
/// reserved.
249+
///
250+
/// * `Scribble.removeTextPlaceholder`: Requests that any placeholder writing
251+
/// space is removed.
252+
static const MethodChannel scribble = OptionalMethodChannel(
253+
'flutter/scribble',
254+
JSONMethodCodec(),
255+
);
256+
225257
/// A [MethodChannel] for handling spell check for text input.
226258
///
227259
/// This channel exposes the spell check framework for supported platforms.

0 commit comments

Comments
 (0)