diff --git a/lib/web_ui/lib/src/engine/browser_detection.dart b/lib/web_ui/lib/src/engine/browser_detection.dart index e2217002bfd58..fd1c8bfe19205 100644 --- a/lib/web_ui/lib/src/engine/browser_detection.dart +++ b/lib/web_ui/lib/src/engine/browser_detection.dart @@ -13,6 +13,9 @@ enum BrowserEngine { /// The engine that powers Safari. webkit, + /// The engine that powers Firefox. + firefox, + /// We were unable to detect the current browser engine. unknown, } @@ -31,6 +34,10 @@ BrowserEngine _detectBrowserEngine() { return BrowserEngine.blink; } else if (vendor == 'Apple Computer, Inc.') { return BrowserEngine.webkit; + } else if (vendor == '') { + // An empty string means firefox: + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vendor + return BrowserEngine.firefox; } // Assume blink otherwise, but issue a warning. diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 11c2af21c9139..75d04623a2bdc 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -254,12 +254,20 @@ flt-semantics input[type=range] { ' -webkit-appearance: none;' '}', sheet.cssRules.length); + } + if (browserEngine == BrowserEngine.firefox) { + sheet.insertRule( + 'input::-moz-selection {' + ' background-color: transparent;' + '}', + sheet.cssRules.length); + } else { // On iOS, the invisible semantic text field has a visible cursor and // selection highlight. The following 2 CSS rules force everything to be // transparent. sheet.insertRule( - 'flt-semantics ::selection {' + 'input::selection {' ' background-color: transparent;' '}', sheet.cssRules.length); diff --git a/lib/web_ui/lib/src/engine/platform_views.dart b/lib/web_ui/lib/src/engine/platform_views.dart index f71c5e0be0e12..56f69258e7585 100644 --- a/lib/web_ui/lib/src/engine/platform_views.dart +++ b/lib/web_ui/lib/src/engine/platform_views.dart @@ -59,15 +59,20 @@ void _createPlatformView( final Map args = methodCall.arguments; final int id = args['id']; final String viewType = args['viewType']; + const MethodCodec codec = StandardMethodCodec(); + // TODO(het): Use 'direction', 'width', and 'height'. if (!platformViewRegistry._registeredFactories.containsKey(viewType)) { - // TODO(het): Do we have a way of nicely reporting errors during platform - // channel calls? - callback(null); + callback(codec.encodeErrorEnvelope( + code: 'Unregistered factory', + message: "No factory registered for viewtype '$viewType'", + )); + return; } // TODO(het): Use creation parameters. final html.Element element = platformViewRegistry._registeredFactories[viewType](id); platformViewRegistry._createdViews[id] = element; + callback(codec.encodeSuccessEnvelope(null)); } diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index f02db3ffdbba0..9cee4586d5936 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -62,6 +62,7 @@ class TextField extends RoleManager { switch (browserEngine) { case BrowserEngine.blink: + case BrowserEngine.firefox: case BrowserEngine.unknown: _initializeForBlink(); break; diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 4a67207ff553e..c41a6b6cf51f8 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -293,7 +293,7 @@ class EngineParagraph implements ui.Paragraph { @override List computeLineMetrics() { - // TODO(flutter_web): Implement this. + // TODO(flutter_web): https://github.com/flutter/flutter/issues/39537 return null; } } @@ -1059,7 +1059,7 @@ void _applyParagraphStyleToElement({ cssStyle.lineHeight = '${style._lineHeight}'; } if (style._textDirection != null) { - cssStyle.direction = _textDirectionToCssValue(style._textDirection); + cssStyle.direction = _textDirectionToCss(style._textDirection); } if (style._fontSize != null) { cssStyle.fontSize = '${style._fontSize.floor()}px'; @@ -1083,7 +1083,7 @@ void _applyParagraphStyleToElement({ cssStyle.lineHeight = '${style._lineHeight}'; } if (style._textDirection != previousStyle._textDirection) { - cssStyle.direction = _textDirectionToCssValue(style._textDirection); + cssStyle.direction = _textDirectionToCss(style._textDirection); } if (style._fontSize != previousStyle._fontSize) { cssStyle.fontSize = @@ -1272,10 +1272,28 @@ String _decorationStyleToCssString(ui.TextDecorationStyle decorationStyle) { /// ```css /// direction: rtl; /// ``` -String _textDirectionToCssValue(ui.TextDirection textDirection) { - return textDirection == ui.TextDirection.ltr - ? null // it's the default - : 'rtl'; +String _textDirectionToCss(ui.TextDirection textDirection) { + if (textDirection == null) { + return null; + } + return textDirectionIndexToCss(textDirection.index); +} + +String textDirectionIndexToCss(int textDirectionIndex) { + switch (textDirectionIndex) { + case 0: + return 'rtl'; + case 1: + return null; // ltr is the default + } + + assert(() { + throw AssertionError( + 'Failed to convert text direction $textDirectionIndex to CSS', + ); + }()); + + return null; } /// Converts [align] to its corresponding CSS value. diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing.dart index 694a3f9f6c790..e9a8244f739d5 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing.dart @@ -14,30 +14,31 @@ void _emptyCallback(dynamic _) {} /// /// They are assigned once during the creation of the dom element. void _setStaticStyleAttributes(html.HtmlElement domElement) { - domElement.style + final html.CssStyleDeclaration elementStyle = domElement.style; + elementStyle ..whiteSpace = 'pre' ..alignContent = 'center' ..position = 'absolute' + ..top = '0' + ..left = '0' ..padding = '0' - ..opacity = '1'; + ..opacity = '1' + ..color = 'transparent' + ..backgroundColor = 'transparent' + ..background = 'transparent' + ..outline = 'none' + ..border = 'none' + ..resize = 'none' + ..textShadow = 'transparent' + ..transformOrigin = '0 0 0'; + + /// This property makes the input's blinking cursor transparent. + elementStyle.setProperty('caret-color', 'transparent'); + if (_debugVisibleTextEditing) { - domElement.style + elementStyle ..color = 'purple' - ..backgroundColor = 'pink'; - } else { - domElement.style - ..color = 'transparent' - ..backgroundColor = 'transparent' - ..background = 'transparent' - ..border = 'none' - ..resize = 'none' - ..cursor = 'none' - ..textShadow = 'transparent' - ..outline = 'none'; - - /// This property makes the cursor transparent in mobile browsers where - /// cursor = 'none' does not work. - domElement.style.setProperty('caret-color', 'transparent'); + ..outline = '1px solid purple'; } } @@ -479,6 +480,7 @@ class PersistentTextEditingElement extends TextEditingElement { }) : _onDomElementSwap = onDomElementSwap, super(owner) { // Make sure the dom element is of a type that we support for text editing. + // TODO(yjbanov): move into initializer list when https://github.com/dart-lang/sdk/issues/37881 is fixed. assert(_getTypeFromElement(domElement) != null); this.domElement = domElement; } @@ -611,7 +613,7 @@ class HybridTextEditing { } break; - case 'TextInput.setEditingLocationSize': + case 'TextInput.setEditableSizeAndTransform': _setLocation(call.arguments); break; @@ -656,41 +658,41 @@ class HybridTextEditing { assert(style.containsKey('fontSize')); assert(style.containsKey('fontFamily')); assert(style.containsKey('textAlignIndex')); + assert(style.containsKey('textDirectionIndex')); - final int textAlignIndex = style.remove('textAlignIndex'); + final int textAlignIndex = style['textAlignIndex']; + final int textDirectionIndex = style['textDirectionIndex']; - /// Converts integer value coming as fontWeightValue from TextInput.setStyle + /// Converts integer value coming as fontWeightIndex from TextInput.setStyle /// to its CSS equivalent value. /// Converts index of TextAlign to enum value. _editingStyle = _EditingStyle( - textDirection: style.containsKey('textDirection') - ? style.remove('textDirection') - : ui.TextDirection.ltr, - fontSize: style.remove('fontSize'), + textDirection: ui.TextDirection.values[textDirectionIndex], + fontSize: style['fontSize'], textAlign: ui.TextAlign.values[textAlignIndex], - fontFamily: style.remove('fontFamily'), - fontWeight: fontWeightIndexToCss( - fontWeightIndex: style.remove('fontWeightValue')), + fontFamily: style['fontFamily'], + fontWeight: + fontWeightIndexToCss(fontWeightIndex: style['fontWeightIndex']), ); } - /// Location of the editable text on the page as a rectangle. - // TODO(flutter_web): investigate if transform matrix can be used instead of - // a rectangle. - _EditingLocationAndSize _editingLocationAndSize; - _EditingLocationAndSize get editingLocationAndSize => _editingLocationAndSize; + /// Size and transform of the editable text on the page. + _EditableSizeAndTransform _editingLocationAndSize; + _EditableSizeAndTransform get editingLocationAndSize => + _editingLocationAndSize; void _setLocation(Map editingLocationAndSize) { - assert(editingLocationAndSize.containsKey('top')); - assert(editingLocationAndSize.containsKey('left')); assert(editingLocationAndSize.containsKey('width')); assert(editingLocationAndSize.containsKey('height')); - - _editingLocationAndSize = _EditingLocationAndSize( - top: editingLocationAndSize.remove('top'), - left: editingLocationAndSize.remove('left'), - width: editingLocationAndSize.remove('width'), - height: editingLocationAndSize.remove('height')); + assert(editingLocationAndSize.containsKey('transform')); + + final List transformList = + List.from(editingLocationAndSize['transform']); + _editingLocationAndSize = _EditableSizeAndTransform( + width: editingLocationAndSize['width'], + height: editingLocationAndSize['height'], + transform: Float64List.fromList(transformList), + ); if (editingElement.domElement != null) { _setDynamicStyleAttributes(editingElement.domElement); @@ -714,7 +716,7 @@ class HybridTextEditing { /// element. /// /// They are changed depending on the messages coming from method calls: - /// "TextInput.setStyle", "TextInput.setEditingLocationSize". + /// "TextInput.setStyle", "TextInput.setEditableSizeAndTransform". void _setDynamicStyleAttributes(html.HtmlElement domElement) { if (_editingLocationAndSize != null && !(browserEngine == BrowserEngine.webkit && @@ -731,18 +733,14 @@ class HybridTextEditing { /// Users can interact with the element and use the functionalities of the /// right-click menu. Such as copy,paste, cut, select, translate... void setStyle(html.HtmlElement domElement) { + final String transformCss = + float64ListToCssTransform(_editingLocationAndSize.transform); domElement.style - ..top = '${_editingLocationAndSize.top}px' - ..left = '${_editingLocationAndSize.left}px' ..width = '${_editingLocationAndSize.width}px' - ..height = '${_editingLocationAndSize.height}px'; - if (_debugVisibleTextEditing) { - domElement.style.font = '24px sans-serif'; - } else { - domElement.style - ..textAlign = _editingStyle.align - ..font = font(); - } + ..height = '${_editingLocationAndSize.height}px' + ..textAlign = _editingStyle.align + ..font = font() + ..transform = transformCss; } html.InputElement createInputElement() { @@ -785,19 +783,18 @@ class _EditingStyle { /// Information on the location and size of the editing element. /// -/// This information is received via "TextInput.setEditingLocationSize" +/// This information is received via "TextInput.setEditableSizeAndTransform" /// message. Framework currently sends this information on paint. // TODO(flutter_web): send the location during the scroll for more frequent // updates from the framework. -class _EditingLocationAndSize { - _EditingLocationAndSize( - {@required this.top, - @required this.left, - @required this.width, - @required this.height}); - - final double top; - final double left; +class _EditableSizeAndTransform { + _EditableSizeAndTransform({ + @required this.width, + @required this.height, + @required this.transform, + }); + final double width; final double height; + final Float64List transform; } diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 62e4e334cb432..6e53730c892bf 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -471,21 +471,22 @@ void main() { 'TextInput.setClient', [123, flutterSinglelineConfig]); textEditing.handleTextInput(codec.encodeMethodCall(setClient)); - const MethodCall setLocationSize = - MethodCall('TextInput.setEditingLocationSize', { - 'top': 0, - 'left': 0, + final MethodCall setSizeAndTransform = + MethodCall('TextInput.setEditableSizeAndTransform', { 'width': 150, 'height': 50, + 'transform': + Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList() }); - textEditing.handleTextInput(codec.encodeMethodCall(setLocationSize)); + textEditing.handleTextInput(codec.encodeMethodCall(setSizeAndTransform)); const MethodCall setStyle = MethodCall('TextInput.setStyle', { 'fontSize': 12, 'fontFamily': 'sans-serif', 'textAlignIndex': 4, - 'fontWeightValue': 4, + 'fontWeightIndex': 4, + 'textDirectionIndex': 1, }); textEditing.handleTextInput(codec.encodeMethodCall(setStyle)); @@ -500,14 +501,17 @@ void main() { const MethodCall show = MethodCall('TextInput.show'); textEditing.handleTextInput(codec.encodeMethodCall(show)); - checkInputEditingState( - textEditing.editingElement.domElement, 'abcd', 2, 3); + final HtmlElement domElement = textEditing.editingElement.domElement; + + checkInputEditingState(domElement, 'abcd', 2, 3); // Check if the location and styling is correct. expect( - textEditing.editingElement.domElement.getBoundingClientRect(), - Rectangle.fromPoints( - const Point(0.0, 0.0), const Point(150.0, 50.0))); + domElement.getBoundingClientRect(), + Rectangle.fromPoints(const Point(10.0, 20.0), + const Point(160.0, 70.0))); + expect(domElement.style.transform, + 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)'); expect(textEditing.editingElement.domElement.style.font, '500 12px sans-serif'); diff --git a/lib/web_ui/test/title_test.dart b/lib/web_ui/test/title_test.dart index 2af3c67ba9d28..906053465ec99 100644 --- a/lib/web_ui/test/title_test.dart +++ b/lib/web_ui/test/title_test.dart @@ -16,6 +16,7 @@ void main() { // Run the unit test without emulating Flutter tester environment. ui.debugEmulateFlutterTesterEnvironment = false; + // TODO(yjbanov): https://github.com/flutter/flutter/issues/39159 document.title = ''; expect(document.title, '');