Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Scribble mixin #104128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/flutter/lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 2 additions & 3 deletions packages/flutter/lib/src/services/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With scribble in a separated object you don't need to eagerly initialize TextInput I think?

readInitialLifecycleStateFromNativeWindow();
}

Expand Down Expand Up @@ -326,7 +326,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
void setSystemUiChangeCallback(SystemUiChangeCallback? callback) {
_systemUiChangeCallback = callback;
}

}

/// Signature for listening to changes in the [SystemUiMode].
Expand Down
243 changes: 243 additions & 0 deletions packages/flutter/lib/src/services/scribble.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation didn't explain offset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but I'll add something about it.


/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds,
/// where the rectangle bounds are in global coordinates.
bool isInScribbleRect(Rect rect);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: specify coordinate space.


/// 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now specifically for scribble.


/// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: space between == and (Object other).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just searched through the repo and every other override of == is formatted like this too. Not sure if it's right but I'll keep it for now for consistency.

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)';
}
32 changes: 32 additions & 0 deletions packages/flutter/lib/src/services/system_channels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading