|
| 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 | +} |
0 commit comments