-
Notifications
You must be signed in to change notification settings - Fork 28.7k
Support Scribble Handwriting #75472
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
Support Scribble Handwriting #75472
Changes from all commits
c233ddf
895a7b8
d0adc30
9f04325
eef7913
f494809
42d522f
7de1f73
6ccdd12
11c2618
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,3 +89,4 @@ Pradumna Saraf <[email protected]> | |
Kai Yu <[email protected]> | ||
Denis Grafov <[email protected]> | ||
TheOneWithTheBraid <[email protected]> | ||
Twin Sun, LLC <[email protected]> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1265,6 +1265,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, | |
// [assembleSemanticsNode] invocations. | ||
Queue<SemanticsNode>? _cachedChildNodes; | ||
|
||
/// Returns a list of rects that bound the given selection. | ||
/// | ||
/// See [TextPainter.getBoxesForSelection] for more details. | ||
List<Rect> getBoxesForSelection(TextSelection selection) { | ||
_computeTextMetricsIfNeeded(); | ||
return _textPainter.getBoxesForSelection(selection) | ||
.map((TextBox textBox) => textBox.toRect().shift(_paintOffset)) | ||
.toList(); | ||
Comment on lines
+1274
to
+1275
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Not sure if this is in the styleguide or not, so I could be wrong, but I thought we only indent by four characters here and don't align the dots. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can change it if you want, the closest thing I could find in the style guide was https://github.com/heartcombo/devise/blob/c82e4cf47b02002b2fd7ca31d441cf1043fc634c/lib/devise/models/recoverable.rb#L145, but it doesn't address this specific case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine if the styleguide doesn't mention it and the analyzer doesn't care. |
||
} | ||
|
||
@override | ||
void describeSemanticsConfiguration(SemanticsConfiguration config) { | ||
super.describeSemanticsConfiguration(config); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -955,6 +955,9 @@ enum SelectionChangedCause { | |
/// The user used the mouse to change the selection by dragging over a piece | ||
/// of text. | ||
drag, | ||
|
||
/// The user used iPadOS 14+ Scribble to change the selection. | ||
scribble, | ||
} | ||
|
||
/// A mixin for manipulating the selection, provided for toolbar or shortcut | ||
|
@@ -1105,6 +1108,76 @@ abstract class TextInputClient { | |
/// | ||
/// [TextInputClient] should cleanup its connection and finalize editing. | ||
void connectionClosed(); | ||
|
||
/// 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() {} | ||
} | ||
|
||
/// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know there is no implementation here, but should this specify inclusively or exclusively? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like it's exclusive in the default implementation that I provided using |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be private? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's used in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I missed that, sounds good. |
||
/// 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 => hashValues(position, bounds); | ||
|
||
@override | ||
String toString() => 'SelectionRect($position, $bounds)'; | ||
} | ||
|
||
/// An interface to receive granular information from [TextInput]. | ||
|
@@ -1154,6 +1227,7 @@ class TextInputConnection { | |
Matrix4? _cachedTransform; | ||
Rect? _cachedRect; | ||
Rect? _cachedCaretRect; | ||
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[]; | ||
|
||
static int _nextId = 1; | ||
final int _id; | ||
|
@@ -1176,6 +1250,12 @@ 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); | ||
|
@@ -1274,6 +1354,19 @@ class TextInputConnection { | |
); | ||
} | ||
|
||
/// 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.map((SelectionRect rect) { | ||
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could probably make it work, but |
||
}).toList()); | ||
} | ||
} | ||
|
||
/// Send text styling information. | ||
/// | ||
/// This information is used by the Flutter Web Engine to change the style | ||
|
@@ -1535,10 +1628,43 @@ class TextInput { | |
TextInputConnection? _currentConnection; | ||
late TextInputConfiguration _currentConfiguration; | ||
|
||
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels a bit strange to store the client list here. Would it be possible to move this to a widget? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would a static member of |
||
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> _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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
}).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; | ||
final String method = methodCall.method; | ||
|
||
// The requestExistingInputState request needs to be handled regardless of | ||
// the client ID, as long as we have a _currentConnection. | ||
|
@@ -1630,6 +1756,15 @@ 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(); | ||
} | ||
|
@@ -1703,6 +1838,13 @@ class TextInput { | |
); | ||
} | ||
|
||
void _setSelectionRects(List<List<num>> args) { | ||
_channel.invokeMethod<void>( | ||
'TextInput.setSelectionRects', | ||
args, | ||
); | ||
} | ||
|
||
void _setStyle(Map<String, dynamic> args) { | ||
_channel.invokeMethod<void>( | ||
'TextInput.setStyle', | ||
|
@@ -1765,4 +1907,18 @@ class TextInput { | |
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be any cleaner if this method were added to TextLayoutMetrics?
The goal of that class is to simplify things by providing a read-only interface instead of the full RenderEditable. You probably have to use RenderEditable anyway though, so maybe it's not worth it here. Just wanted to bring it up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah...yeah it looks like
RenderEditable
is the only implementer ofTextLayoutMetrics
, so I suppose that would make some sense. I don't know how useful this is for things other than Scribble interactions, though. The implementation would probably need to stay here since it needs theTextPainter
, though.