mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Upstream changes necessary for text editing in flutter web (#39344)
This commit is contained in:
parent
bb1d139c50
commit
a2957c571e
@ -4,9 +4,17 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:ui' show TextAffinity, hashValues, Offset;
|
||||
import 'dart:ui' show
|
||||
FontWeight,
|
||||
Offset,
|
||||
Size,
|
||||
TextAffinity,
|
||||
TextAlign,
|
||||
TextDirection,
|
||||
hashValues;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' show Matrix4;
|
||||
|
||||
import 'message_codec.dart';
|
||||
import 'system_channels.dart';
|
||||
@ -660,6 +668,52 @@ class TextInputConnection {
|
||||
);
|
||||
}
|
||||
|
||||
/// Send the size and transform of the editable text to engine.
|
||||
///
|
||||
/// The values are sent as platform messages so they can be used on web for
|
||||
/// example to correctly position and size the html input field.
|
||||
///
|
||||
/// 1. [editableBoxSize]: size of the render editable box.
|
||||
///
|
||||
/// 2. [transform]: a matrix that maps the local paint coordinate system
|
||||
/// to the [PipelineOwner.rootNode].
|
||||
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
|
||||
SystemChannels.textInput.invokeMethod<void>(
|
||||
'TextInput.setEditableSizeAndTransform',
|
||||
<String, dynamic>{
|
||||
'width': editableBoxSize.width,
|
||||
'height': editableBoxSize.height,
|
||||
'transform': transform.storage,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Send text styling information.
|
||||
///
|
||||
/// This information is used by the Flutter Web Engine to change the style
|
||||
/// of the hidden native input's content. Hence, the content size will match
|
||||
/// to the size of the editable widget's content.
|
||||
void setStyle({
|
||||
@required String fontFamily,
|
||||
@required double fontSize,
|
||||
@required FontWeight fontWeight,
|
||||
@required TextDirection textDirection,
|
||||
@required TextAlign textAlign,
|
||||
}) {
|
||||
assert(attached);
|
||||
|
||||
SystemChannels.textInput.invokeMethod<void>(
|
||||
'TextInput.setStyle',
|
||||
<String, dynamic>{
|
||||
'fontFamily': fontFamily,
|
||||
'fontSize': fontSize,
|
||||
'fontWeightIndex': fontWeight?.index,
|
||||
'textAlignIndex': textAlign.index,
|
||||
'textDirectionIndex': textDirection.index,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Stop interacting with the text input control.
|
||||
///
|
||||
/// After calling this method, the text input control might disappear if no
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ui' as ui hide TextStyle;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
@ -1095,6 +1095,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
if (oldWidget.readOnly && _hasFocus)
|
||||
_openInputConnection();
|
||||
}
|
||||
if (widget.style != oldWidget.style) {
|
||||
final TextStyle style = widget.style;
|
||||
_textInputConnection?.setStyle(
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
fontWeight: style.fontWeight,
|
||||
textDirection: _textDirection,
|
||||
textAlign: widget.textAlign,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1335,7 +1345,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
textCapitalization: widget.textCapitalization,
|
||||
keyboardAppearance: widget.keyboardAppearance,
|
||||
),
|
||||
)..setEditingState(localValue);
|
||||
);
|
||||
|
||||
_updateSizeAndTransform();
|
||||
final TextStyle style = widget.style;
|
||||
_textInputConnection
|
||||
..setStyle(
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
fontWeight: style.fontWeight,
|
||||
textDirection: _textDirection,
|
||||
textAlign: widget.textAlign,
|
||||
)
|
||||
..setEditingState(localValue);
|
||||
}
|
||||
_textInputConnection.show();
|
||||
}
|
||||
@ -1626,6 +1648,23 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
updateKeepAlive();
|
||||
}
|
||||
|
||||
Size _lastSize;
|
||||
Matrix4 _lastTransform;
|
||||
|
||||
void _updateSizeAndTransform() {
|
||||
if (_hasInputConnection) {
|
||||
final Size size = renderEditable.size;
|
||||
final Matrix4 transform = renderEditable.getTransformTo(null);
|
||||
if (size != _lastSize || transform != _lastTransform) {
|
||||
_lastSize = size;
|
||||
_lastTransform = transform;
|
||||
_textInputConnection.setEditableSizeAndTransform(size, transform);
|
||||
}
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
|
||||
}
|
||||
}
|
||||
|
||||
TextDirection get _textDirection {
|
||||
final TextDirection result = widget.textDirection ?? Directionality.of(context);
|
||||
assert(result != null, '$runtimeType created without a textDirection and with no ambient Directionality.');
|
||||
@ -1659,6 +1698,14 @@ 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.
|
||||
bool showToolbar() {
|
||||
// Web is using native dom elements to enable clipboard functionality of the
|
||||
// toolbar: copy, paste, select, cut. It might also provide additional
|
||||
// functionality depending on the browser (such as translate). Due to this
|
||||
// we should not show a Flutter toolbar for the editable text elements.
|
||||
if (kIsWeb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_selectionOverlay == null || _selectionOverlay.toolbarIsVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -534,6 +534,13 @@ void main() {
|
||||
equals('TextInputAction.done'));
|
||||
});
|
||||
|
||||
/// Toolbar is not used in Flutter Web. Skip this check.
|
||||
///
|
||||
/// Web is using native dom elements (it is also used as platform input)
|
||||
/// to enable clipboard functionality of the toolbar: copy, paste, select,
|
||||
/// cut. It might also provide additional functionality depending on the
|
||||
/// browser (such as translation). Due to this, in browsers, we should not
|
||||
/// show a Flutter toolbar for the editable text elements.
|
||||
testWidgets('can show toolbar when there is text and a selection', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@ -577,7 +584,7 @@ void main() {
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pump();
|
||||
expect(find.text('PASTE'), findsOneWidget);
|
||||
});
|
||||
}, skip: isBrowser);
|
||||
|
||||
testWidgets('can show the toolbar after clearing all text', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/35998.
|
||||
@ -999,21 +1006,22 @@ void main() {
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(); // An extra pump to allow focus request to go through.
|
||||
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
|
||||
// Verify TextInput.setEditingState is fired with updated text when controller is replaced.
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
});
|
||||
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
|
||||
// Verify TextInput.setEditingState and TextInput.setEditableSizeAndTransform are
|
||||
// both fired with updated text when controller is replaced.
|
||||
setState(() {
|
||||
currentController = controller2;
|
||||
});
|
||||
await tester.pump();
|
||||
|
||||
expect(log, hasLength(1));
|
||||
expect(
|
||||
log.single,
|
||||
log.lastWhere((MethodCall m) => m.method == 'TextInput.setEditingState'),
|
||||
isMethodCall(
|
||||
'TextInput.setEditingState',
|
||||
arguments: const <String, dynamic>{
|
||||
@ -1027,6 +1035,17 @@ void main() {
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(
|
||||
log.lastWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform'),
|
||||
isMethodCall(
|
||||
'TextInput.setEditableSizeAndTransform',
|
||||
arguments: <String, dynamic>{
|
||||
'width': 800,
|
||||
'height': 14,
|
||||
'transform': Matrix4.translationValues(0.0, 293.0, 0.0).storage.toList(),
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
|
||||
@ -2093,6 +2112,196 @@ void main() {
|
||||
expect(setClient.arguments.last['keyboardAppearance'], 'Brightness.light');
|
||||
});
|
||||
|
||||
testWidgets('location of widget is sent on show keyboard', (WidgetTester tester) async {
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
});
|
||||
|
||||
final TextEditingController controller = TextEditingController();
|
||||
await tester.pumpWidget(
|
||||
MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
devicePixelRatio: 1.0
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: EditableText(
|
||||
controller: controller,
|
||||
focusNode: FocusNode(),
|
||||
style: Typography(platform: TargetPlatform.android).black.subhead,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
final MethodCall methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
|
||||
expect(
|
||||
methodCall,
|
||||
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
|
||||
'width': 800,
|
||||
'height': 600,
|
||||
'transform': Matrix4.identity().storage.toList(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('size and transform are sent when they change', (WidgetTester tester) async {
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
});
|
||||
|
||||
const Offset offset = Offset(10.0, 20.0);
|
||||
const Key transformButtonKey = Key('transformButton');
|
||||
await tester.pumpWidget(
|
||||
const TransformedEditableText(
|
||||
offset: offset,
|
||||
transformButtonKey: transformButtonKey,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
MethodCall methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
|
||||
expect(
|
||||
methodCall,
|
||||
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
|
||||
'width': 800,
|
||||
'height': 14,
|
||||
'transform': Matrix4.identity().storage.toList(),
|
||||
}),
|
||||
);
|
||||
|
||||
log.clear();
|
||||
await tester.tap(find.byKey(transformButtonKey));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// There should be a new platform message updating the transform.
|
||||
methodCall = log.firstWhere((MethodCall m) => m.method == 'TextInput.setEditableSizeAndTransform');
|
||||
expect(
|
||||
methodCall,
|
||||
isMethodCall('TextInput.setEditableSizeAndTransform', arguments: <String, dynamic>{
|
||||
'width': 800,
|
||||
'height': 14,
|
||||
'transform': Matrix4.translationValues(offset.dx, offset.dy, 0.0).storage.toList(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async {
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
});
|
||||
|
||||
final TextEditingController controller = TextEditingController();
|
||||
await tester.pumpWidget(
|
||||
MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: EditableText(
|
||||
textDirection: TextDirection.rtl,
|
||||
controller: controller,
|
||||
focusNode: FocusNode(),
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontFamily: 'Roboto',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
final MethodCall setStyle = log.firstWhere((MethodCall m) => m.method == 'TextInput.setStyle');
|
||||
expect(
|
||||
setStyle,
|
||||
isMethodCall('TextInput.setStyle', arguments: <String, dynamic>{
|
||||
'fontSize': 20.0,
|
||||
'fontFamily': 'Roboto',
|
||||
'fontWeightIndex': 5,
|
||||
'textAlignIndex': 4,
|
||||
'textDirectionIndex': 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('text styling info is sent on style update', (WidgetTester tester) async {
|
||||
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
||||
StateSetter setState;
|
||||
const TextStyle textStyle1 = TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontFamily: 'RobotoMono',
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
const TextStyle textStyle2 = TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontFamily: 'Raleway',
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
TextStyle currentTextStyle = textStyle1;
|
||||
|
||||
Widget builder() {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setter) {
|
||||
setState = setter;
|
||||
return MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
key: editableTextKey,
|
||||
controller: controller,
|
||||
focusNode: FocusNode(),
|
||||
style: currentTextStyle,
|
||||
cursorColor: Colors.blue,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
keyboardType: TextInputType.text,
|
||||
onChanged: (String value) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
|
||||
final List<MethodCall> log = <MethodCall>[];
|
||||
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
log.add(methodCall);
|
||||
});
|
||||
setState(() {
|
||||
currentTextStyle = textStyle2;
|
||||
});
|
||||
await tester.pump();
|
||||
|
||||
// Updated styling information should be sent via TextInput.setStyle method.
|
||||
final MethodCall setStyle = log.firstWhere((MethodCall m) => m.method == 'TextInput.setStyle');
|
||||
expect(
|
||||
setStyle,
|
||||
isMethodCall('TextInput.setStyle', arguments: <String, dynamic>{
|
||||
'fontSize': 20.0,
|
||||
'fontFamily': 'Raleway',
|
||||
'fontWeightIndex': 6,
|
||||
'textAlignIndex': 4,
|
||||
'textDirectionIndex': 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('custom keyboardAppearance is respected', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/22212.
|
||||
|
||||
@ -2659,3 +2868,54 @@ class CustomStyleEditableTextState extends EditableTextState {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TransformedEditableText extends StatefulWidget {
|
||||
const TransformedEditableText({ this.offset, this.transformButtonKey });
|
||||
|
||||
final Offset offset;
|
||||
final Key transformButtonKey;
|
||||
|
||||
@override
|
||||
_TransformedEditableTextState createState() => _TransformedEditableTextState();
|
||||
}
|
||||
|
||||
class _TransformedEditableTextState extends State<TransformedEditableText> {
|
||||
bool _isTransformed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
devicePixelRatio: 1.0
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Transform.translate(
|
||||
offset: _isTransformed ? widget.offset : Offset.zero,
|
||||
child: EditableText(
|
||||
controller: TextEditingController(),
|
||||
focusNode: FocusNode(),
|
||||
style: Typography(platform: TargetPlatform.android).black.subhead,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
),
|
||||
),
|
||||
RaisedButton(
|
||||
key: widget.transformButtonKey,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isTransformed = !_isTransformed;
|
||||
});
|
||||
},
|
||||
child: const Text('Toggle Transform'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user