diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 3b2a5c8fce40ded1f2a6081231572b118b2ba323..381398b7396a610d4036ccb7d289c2dd7f6aaba1 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -141,7 +141,15 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { if (_ctx != null) { _ctx.restore(); _ctx.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels); - _ctx.font = ''; + try { + _ctx.font = ''; + } catch (e) { + // Firefox may explode here: + // https://bugzilla.mozilla.org/show_bug.cgi?id=941146 + if (!_isNsErrorFailureException(e)) { + rethrow; + } + } _initializeViewport(); } if (_canvas != null) { @@ -450,12 +458,10 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { _strokeOrFill(paint); } - void _drawRRectPath(ui.RRect rrect, {bool startNewPath = true}) { - // TODO(mdebbar): there's a bug in this code, it doesn't correctly handle - // the case when the radius is greater than the width of the - // rect. When we fix that in houdini_painter.js, we need to - // fix it here too. - // To draw the rounded rectangle, perform the following 8 steps: + void _drawRRectPath(ui.RRect inputRRect, {bool startNewPath = true}) { + // TODO(mdebbar): Backport the overlapping corners fix to houdini_painter.js + // To draw the rounded rectangle, perform the following steps: + // 0. Ensure border radius don't overlap // 1. Flip left,right top,bottom since web doesn't support flipped // coordinates with negative radii. // 2. draw the line for the top @@ -471,6 +477,9 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { // rounded rectangle (after the corner). // TODO(het): Confirm that this is the end point in Flutter for RRect + // Ensure border radius curves never overlap + final ui.RRect rrect = inputRRect.scaleRadii(); + double left = rrect.left; double right = rrect.right; double top = rrect.top; @@ -551,7 +560,10 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { ); } - void _drawRRectPathReverse(ui.RRect rrect, {bool startNewPath = true}) { + void _drawRRectPathReverse(ui.RRect inputRRect, {bool startNewPath = true}) { + // Ensure border radius curves never overlap + final ui.RRect rrect = inputRRect.scaleRadii(); + double left = rrect.left; double right = rrect.right; double top = rrect.top; diff --git a/lib/web_ui/lib/src/engine/browser_detection.dart b/lib/web_ui/lib/src/engine/browser_detection.dart index 967aea7dc949e1911f0b2503f24eae204b3b5dc7..e2217002bfd58182a2688043661d132d584c9599 100644 --- a/lib/web_ui/lib/src/engine/browser_detection.dart +++ b/lib/web_ui/lib/src/engine/browser_detection.dart @@ -38,3 +38,57 @@ BrowserEngine _detectBrowserEngine() { return BrowserEngine.unknown; } + +/// Operating system where the current browser runs. +/// +/// Taken from the navigator platform. +/// +enum OperatingSystem { + /// iOS: + iOs, + + /// Android: + android, + + /// Linux: + linux, + + /// Windows: + windows, + + /// MacOs: + macOs, + + /// We were unable to detect the current operating system. + unknown, +} + +/// Lazily initialized current operating system. +OperatingSystem _operatingSystem; + +/// Returns the [OperatingSystem] the current browsers works on. +/// +/// This is used to implement operating system specific behavior such as +/// soft keyboards. +OperatingSystem get operatingSystem => + _operatingSystem ??= _detectOperatingSystem(); + +OperatingSystem _detectOperatingSystem() { + final String platform = html.window.navigator.platform; + + if (platform.startsWith('Mac')) { + return OperatingSystem.macOs; + } else if (platform.toLowerCase().contains('iphone') || + platform.toLowerCase().contains('ipad') || + platform.toLowerCase().contains('ipod')) { + return OperatingSystem.iOs; + } else if (platform.toLowerCase().contains('android')) { + return OperatingSystem.android; + } else if (platform.startsWith('Linux')) { + return OperatingSystem.linux; + } else if (platform.startsWith('Win')) { + return OperatingSystem.windows; + } else { + return OperatingSystem.unknown; + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/fonts.dart b/lib/web_ui/lib/src/engine/compositor/fonts.dart index b37425ba92c9b92ce82cfd518c438c9ffe2acb7e..e710ce60bf49d5875954fb79dc4b2e7a38669434 100644 --- a/lib/web_ui/lib/src/engine/compositor/fonts.dart +++ b/lib/web_ui/lib/src/engine/compositor/fonts.dart @@ -1,6 +1,7 @@ // Copyright 2013 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. + part of engine; class SkiaFontCollection { diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index fb5f493ccd0386a7afe6e6258077e82441144fc5..6a20012e59ece0c86c319bd80f62a5ad8a0abf0f 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -1,6 +1,7 @@ // Copyright 2013 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. + part of engine; /// Instantiates a [ui.Codec] backed by an `SkImage` from Skia. diff --git a/lib/web_ui/lib/src/engine/compositor/layer.dart b/lib/web_ui/lib/src/engine/compositor/layer.dart index 9bd04f42ea1420897e20cce05331d3087efd9c37..262d5ae77a6f3dacd6ff74ad02263043c9b5dc1a 100644 --- a/lib/web_ui/lib/src/engine/compositor/layer.dart +++ b/lib/web_ui/lib/src/engine/compositor/layer.dart @@ -181,7 +181,7 @@ class OpacityLayer extends ContainerLayer implements ui.OpacityEngineLayer { @override void preroll(PrerollContext prerollContext, Matrix4 matrix) { - Matrix4 childMatrix = Matrix4.copy(matrix); + final Matrix4 childMatrix = Matrix4.copy(matrix); childMatrix.translate(_offset.dx, _offset.dy); final ui.Rect childPaintBounds = prerollChildren(prerollContext, childMatrix); @@ -189,22 +189,22 @@ class OpacityLayer extends ContainerLayer implements ui.OpacityEngineLayer { } @override - void paint(PaintContext context) { + void paint(PaintContext paintContext) { assert(needsPainting); final ui.Paint paint = ui.Paint(); paint.color = ui.Color.fromARGB(_alpha, 0, 0, 0); - context.canvas.save(); - context.canvas.translate(_offset.dx, _offset.dy); + paintContext.canvas.save(); + paintContext.canvas.translate(_offset.dx, _offset.dy); final ui.Rect saveLayerBounds = paintBounds.shift(-_offset); - context.canvas.saveLayer(saveLayerBounds, paint); - paintChildren(context); + paintContext.canvas.saveLayer(saveLayerBounds, paint); + paintChildren(paintContext); // Restore twice: once for the translate and once for the saveLayer. - context.canvas.restore(); - context.canvas.restore(); + paintContext.canvas.restore(); + paintContext.canvas.restore(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/compositor/util.dart index 0d9896935184154365fd3cd5bf7939635b6a4f67..3f6c2b365d224015f074be1b0966bf5824aa941a 100644 --- a/lib/web_ui/lib/src/engine/compositor/util.dart +++ b/lib/web_ui/lib/src/engine/compositor/util.dart @@ -1,6 +1,7 @@ // Copyright 2013 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. + part of engine; js.JsObject makeSkRect(ui.Rect rect) { @@ -9,7 +10,7 @@ js.JsObject makeSkRect(ui.Rect rect) { } js.JsArray makeSkPoint(ui.Offset point) { - js.JsArray skPoint = new js.JsArray(); + final js.JsArray skPoint = js.JsArray(); skPoint.length = 2; skPoint[0] = point.dx; skPoint[1] = point.dy; diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 8252d7ea4793fe3c75c233f07bb1ae5d3d7753e5..11c2af21c91391c3660fbcd55de16ccb5b2645ef 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -309,6 +309,21 @@ flt-glass-pane * { setElementStyle(bodyElement, 'font', defaultCssFont); setElementStyle(bodyElement, 'color', 'red'); + // TODO(flutter_web): send the location during the scroll for more frequent + // location updates from the framework. Remove spellcheck=false property. + /// The spell check is being disabled for now. + /// + /// Flutter web is positioning the input box on top of editable widget. + /// This location is updated only in the paint phase of the widget. + /// It is wrong during the scroll. It is not important for text editing + /// since the content is already invisible. On the other hand, the red + /// indicator for spellcheck gets confusing due to the wrong positioning. + /// We are disabling spellcheck until the location starts getting updated + /// via scroll. This is possible since we can listen to the scroll on + /// Flutter. + /// See [HybridTextEditing]. + bodyElement.spellcheck = false; + for (html.Element viewportMeta in html.document.head.querySelectorAll('meta[name="viewport"]')) { if (assertionsEnabled) { diff --git a/lib/web_ui/lib/src/engine/onscreen_logging.dart b/lib/web_ui/lib/src/engine/onscreen_logging.dart index 8c65a945ca9d47a3ed9a7c38a25e72b26e8be876..d4d796edcbc6f3a78e884e2f04ce80bd6f39f84d 100644 --- a/lib/web_ui/lib/src/engine/onscreen_logging.dart +++ b/lib/web_ui/lib/src/engine/onscreen_logging.dart @@ -86,10 +86,14 @@ void _initialize() { /// /// The `label` argument, if present, will be printed before the stack. void debugPrintStack({String label, int maxFrames}) { - if (label != null) print(label); + if (label != null) { + print(label); + } Iterable lines = StackTrace.current.toString().trimRight().split('\n'); - if (maxFrames != null) lines = lines.take(maxFrames); + if (maxFrames != null) { + lines = lines.take(maxFrames); + } print(defaultStackFilter(lines).join('\n')); } @@ -132,7 +136,9 @@ Iterable defaultStackFilter(Iterable frames) { result.add('(elided one frame from ${skipped.single})'); } else if (skipped.length > 1) { final List where = Set.from(skipped).toList()..sort(); - if (where.length > 1) where[where.length - 1] = 'and ${where.last}'; + if (where.length > 1) { + where[where.length - 1] = 'and ${where.last}'; + } if (where.length > 2) { result.add('(elided ${skipped.length} frames from ${where.join(", ")})'); } else { diff --git a/lib/web_ui/lib/src/engine/platform_views.dart b/lib/web_ui/lib/src/engine/platform_views.dart index 8cebc951e2844c24bb1deb3333be96d3757d38f2..f71c5e0be0e127d32cba58e185dda84cd79b1788 100644 --- a/lib/web_ui/lib/src/engine/platform_views.dart +++ b/lib/web_ui/lib/src/engine/platform_views.dart @@ -56,7 +56,7 @@ void handlePlatformViewCall( void _createPlatformView( MethodCall methodCall, ui.PlatformMessageResponseCallback callback) { - final Map args = methodCall.arguments; + final Map args = methodCall.arguments; final int id = args['id']; final String viewType = args['viewType']; // TODO(het): Use 'direction', 'width', and 'height'. diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 3e2ee972fb66faef046a75c6f0fab0a0d6e62822..5a8cdf8cb4ff120fa487365c3d7703425f7afc17 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -323,6 +323,11 @@ class TouchAdapter extends BaseAdapter { event.preventDefault(); _updateButtonDownState(_kPrimaryMouseButton, false); _callback(_convertEventToPointerData(ui.PointerChange.up, event)); + if (textEditing.needsKeyboard && + browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs) { + textEditing.editingElement.configureInputElementForIOS(); + } }); _addEventListener('touchcancel', (html.Event event) { diff --git a/lib/web_ui/lib/src/engine/recording_canvas.dart b/lib/web_ui/lib/src/engine/recording_canvas.dart index f845868a3b6c75f406cb316b8084d55c1b3826a6..021ed6262e2bf399dcb9221eae5830850c645607 100644 --- a/lib/web_ui/lib/src/engine/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/recording_canvas.dart @@ -61,8 +61,16 @@ class RecordingCanvas { debugBuf.writeln('--- End of command stream'); print(debugBuf); } else { - for (int i = 0; i < _commands.length; i++) { - _commands[i].apply(engineCanvas); + try { + for (int i = 0; i < _commands.length; i++) { + _commands[i].apply(engineCanvas); + } + } catch (e) { + // commands should never fail, but... + // https://bugzilla.mozilla.org/show_bug.cgi?id=941146 + if (!_isNsErrorFailureException(e)) { + rethrow; + } } } } @@ -1446,13 +1454,8 @@ class _PaintBounds { double transformedPointBottom = bottom; if (!_currentMatrixIsIdentity) { - final ui.Rect transformedRect = localClipToGlobalClip( - localLeft: left, - localTop: top, - localRight: right, - localBottom: bottom, - transform: _currentMatrix, - ); + final ui.Rect transformedRect = + transformLTRB(_currentMatrix, left, top, right, bottom); transformedPointLeft = transformedRect.left; transformedPointTop = transformedRect.top; transformedPointRight = transformedRect.right; 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 a30dee46b92c404d7c49f0ccff0ab54d80b7cc95..f02db3ffdbba077c145a98ab8f6c09b8a33f37dc 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -20,6 +20,7 @@ class TextField extends RoleManager { ? html.TextAreaElement() : html.InputElement(); persistentTextEditingElement = PersistentTextEditingElement( + textEditing, editableDomElement, onDomElementSwap: _setupDomElement, ); diff --git a/lib/web_ui/lib/src/engine/services/buffers.dart b/lib/web_ui/lib/src/engine/services/buffers.dart index e2b053b979fc7be99b201330a2d25421eef34891..fef606c14ad2f3519f78b29778b7ee0fbc914993 100644 --- a/lib/web_ui/lib/src/engine/services/buffers.dart +++ b/lib/web_ui/lib/src/engine/services/buffers.dart @@ -253,8 +253,10 @@ abstract class _TypedDataBuffer extends ListBase { /// /// Grows the buffer if necessary, preserving existing data. void _ensureCapacity(int requiredCapacity) { - if (requiredCapacity <= _buffer.length) return; - var newBuffer = _createBiggerBuffer(requiredCapacity); + if (requiredCapacity <= _buffer.length) { + return; + } + final List newBuffer = _createBiggerBuffer(requiredCapacity); newBuffer.setRange(0, _length, _buffer); _buffer = newBuffer; } diff --git a/lib/web_ui/lib/src/engine/services/message_codecs.dart b/lib/web_ui/lib/src/engine/services/message_codecs.dart index 223618f9bc0df1490ffbb766173a9651f36ee9a7..119e144b234a56165ef1d6b5e1c4cfd4a6416a23 100644 --- a/lib/web_ui/lib/src/engine/services/message_codecs.dart +++ b/lib/web_ui/lib/src/engine/services/message_codecs.dart @@ -271,7 +271,9 @@ class StandardMessageCodec implements MessageCodec { @override ByteData encodeMessage(dynamic message) { - if (message == null) return null; + if (message == null) { + return null; + } final WriteBuffer buffer = WriteBuffer(); writeValue(buffer, message); return buffer.done(); @@ -279,10 +281,14 @@ class StandardMessageCodec implements MessageCodec { @override dynamic decodeMessage(ByteData message) { - if (message == null) return null; + if (message == null) { + return null; + } final ReadBuffer buffer = ReadBuffer(message); final dynamic result = readValue(buffer); - if (buffer.hasRemaining) throw const FormatException('Message corrupted'); + if (buffer.hasRemaining) { + throw const FormatException('Message corrupted'); + } return result; } @@ -350,7 +356,7 @@ class StandardMessageCodec implements MessageCodec { writeValue(buffer, value); }); } else { - throw new ArgumentError.value(value); + throw ArgumentError.value(value); } } @@ -359,7 +365,9 @@ class StandardMessageCodec implements MessageCodec { /// This method is intended for use by subclasses overriding /// [readValueOfType]. dynamic readValue(ReadBuffer buffer) { - if (!buffer.hasRemaining) throw const FormatException('Message corrupted'); + if (!buffer.hasRemaining) { + throw const FormatException('Message corrupted'); + } final int type = buffer.getUint8(); return readValueOfType(type, buffer); } @@ -543,7 +551,9 @@ class StandardMethodCodec implements MethodCodec { if (envelope.lengthInBytes == 0) throw const FormatException('Expected envelope, got nothing'); final ReadBuffer buffer = ReadBuffer(envelope); - if (buffer.getUint8() == 0) return messageCodec.readValue(buffer); + if (buffer.getUint8() == 0) { + return messageCodec.readValue(buffer); + } final dynamic errorCode = messageCodec.readValue(buffer); final dynamic errorMessage = messageCodec.readValue(buffer); final dynamic errorDetails = messageCodec.readValue(buffer); diff --git a/lib/web_ui/lib/src/engine/shader.dart b/lib/web_ui/lib/src/engine/shader.dart index 5050d8c5ed95e8b3bea9ae3be0edf905115ddb86..7eb5aaca6f225cc9c03756a4d094c5f93e5598cb 100644 --- a/lib/web_ui/lib/src/engine/shader.dart +++ b/lib/web_ui/lib/src/engine/shader.dart @@ -1,6 +1,7 @@ // Copyright 2013 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. + part of engine; bool _offsetIsValid(ui.Offset offset) { @@ -46,7 +47,7 @@ class GradientSweep extends EngineGradient { } @override - Object createPaintStyle(_) { + Object createPaintStyle(html.CanvasRenderingContext2D ctx) { throw UnimplementedError(); } @@ -135,7 +136,7 @@ class GradientLinear extends EngineGradient { js.JsObject createSkiaShader() { assert(experimentalUseSkia); - js.JsArray jsColors = js.JsArray(); + final js.JsArray jsColors = js.JsArray(); jsColors.length = colors.length; for (int i = 0; i < colors.length; i++) { jsColors[i] = colors[i].value; @@ -174,7 +175,7 @@ class GradientRadial extends EngineGradient { final Float64List matrix4; @override - Object createPaintStyle(_) { + Object createPaintStyle(html.CanvasRenderingContext2D ctx) { throw UnimplementedError(); } @@ -199,7 +200,7 @@ class GradientConical extends EngineGradient { final Float64List matrix4; @override - Object createPaintStyle(_) { + Object createPaintStyle(html.CanvasRenderingContext2D ctx) { throw UnimplementedError(); } diff --git a/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart b/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart index 4369ae1b4628448d382daaa3f6d8059e909cc676..1b87e2563752ff23f92079abd9b85a36e169ea4e 100644 --- a/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart +++ b/lib/web_ui/lib/src/engine/surface/backdrop_filter.dart @@ -24,6 +24,10 @@ class PersistedBackdropFilter extends PersistedContainerSurface // Reference to transform last used to cache [_invertedTransform]. Matrix4 _previousTransform; + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.identity(); + @override void adoptElements(PersistedBackdropFilter oldSurface) { super.adoptElements(oldSurface); @@ -64,10 +68,8 @@ class PersistedBackdropFilter extends PersistedContainerSurface _invertedTransform = Matrix4.inverted(_transform); _previousTransform = _transform; } - final ui.Rect rect = localClipRectToGlobalClip( - localClip: ui.Rect.fromLTRB( - 0, 0, ui.window.physicalSize.width, ui.window.physicalSize.height), - transform: _invertedTransform); + final ui.Rect rect = transformLTRB(_invertedTransform, 0, 0, + ui.window.physicalSize.width, ui.window.physicalSize.height); final html.CssStyleDeclaration filterElementStyle = _filterElement.style; filterElementStyle ..position = 'absolute' diff --git a/lib/web_ui/lib/src/engine/surface/clip.dart b/lib/web_ui/lib/src/engine/surface/clip.dart index 4255b73838a4747e0744a9ed5780a30d1df6ead2..66db0c1cb0331796018e26a201003738884f728a 100644 --- a/lib/web_ui/lib/src/engine/surface/clip.dart +++ b/lib/web_ui/lib/src/engine/surface/clip.dart @@ -66,12 +66,15 @@ class PersistedClipRect extends PersistedContainerSurface @override void recomputeTransformAndClip() { _transform = parent._transform; - _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip( - localClip: rect, - transform: _transform, - )); + _localClipBounds = rect; + _localTransformInverse = null; + _projectedClip = null; } + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.identity(); + @override html.Element createElement() { return super.createElement()..setAttribute('clip-type', 'rect'); @@ -114,12 +117,15 @@ class PersistedClipRRect extends PersistedContainerSurface @override void recomputeTransformAndClip() { _transform = parent._transform; - _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip( - localClip: rrect.outerRect, - transform: _transform, - )); + _localClipBounds = rrect.outerRect; + _localTransformInverse = null; + _projectedClip = null; } + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.identity(); + @override html.Element createElement() { return super.createElement()..setAttribute('clip-type', 'rrect'); @@ -174,23 +180,23 @@ class PersistedPhysicalShape extends PersistedContainerSurface final ui.RRect roundRect = path.webOnlyPathAsRoundedRect; if (roundRect != null) { - _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip( - localClip: roundRect.outerRect, - transform: transform, - )); + _localClipBounds = roundRect.outerRect; } else { final ui.Rect rect = path.webOnlyPathAsRect; if (rect != null) { - _globalClip = parent._globalClip.intersect(localClipRectToGlobalClip( - localClip: rect, - transform: transform, - )); + _localClipBounds = rect; } else { - _globalClip = parent._globalClip; + _localClipBounds = null; } } + _localTransformInverse = null; + _projectedClip = null; } + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.identity(); + void _applyColor() { rootElement.style.backgroundColor = color.toCssString(); } @@ -338,6 +344,16 @@ class PersistedClipPath extends PersistedContainerSurface return defaultCreateElement('flt-clippath'); } + @override + void recomputeTransformAndClip() { + super.recomputeTransformAndClip(); + _localClipBounds ??= clipPath.getBounds(); + } + + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.identity(); + @override void apply() { if (clipPath == null) { @@ -364,6 +380,7 @@ class PersistedClipPath extends PersistedContainerSurface void update(PersistedClipPath oldSurface) { super.update(oldSurface); if (oldSurface.clipPath != clipPath) { + _localClipBounds = null; oldSurface._clipElement?.remove(); apply(); } else { diff --git a/lib/web_ui/lib/src/engine/surface/offset.dart b/lib/web_ui/lib/src/engine/surface/offset.dart index a239b8b3f460bbde96726d16b1021f062ede0e21..182d64491f9a34739a1ffb8379fe045b27118878 100644 --- a/lib/web_ui/lib/src/engine/surface/offset.dart +++ b/lib/web_ui/lib/src/engine/surface/offset.dart @@ -22,9 +22,14 @@ class PersistedOffset extends PersistedContainerSurface _transform = _transform.clone(); _transform.translate(dx, dy); } - _globalClip = parent._globalClip; + _projectedClip = null; + _localTransformInverse = null; } + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.translationValues(-dx, -dy, 0); + @override html.Element createElement() { return defaultCreateElement('flt-offset')..style.transformOrigin = '0 0 0'; diff --git a/lib/web_ui/lib/src/engine/surface/opacity.dart b/lib/web_ui/lib/src/engine/surface/opacity.dart index 4c304eb67bb09f3f0de98c20b983ce17b8f54452..f8a75a4e83bb87add63a5e4484c6cc1a489bdb9c 100644 --- a/lib/web_ui/lib/src/engine/surface/opacity.dart +++ b/lib/web_ui/lib/src/engine/surface/opacity.dart @@ -24,10 +24,14 @@ class PersistedOpacity extends PersistedContainerSurface _transform = _transform.clone(); _transform.translate(dx, dy); } - - _globalClip = parent._globalClip; + _localTransformInverse = null; + _projectedClip = null; } + @override + Matrix4 get localTransformInverse => _localTransformInverse ??= + Matrix4.translationValues(-offset.dx, -offset.dy, 0); + @override html.Element createElement() { return defaultCreateElement('flt-opacity')..style.transformOrigin = '0 0 0'; diff --git a/lib/web_ui/lib/src/engine/surface/picture.dart b/lib/web_ui/lib/src/engine/surface/picture.dart index 534f9b7bf7822a91e1d147ccef2f03efda2e1807..1031807ee4cb064f7a2b193ba9b932cf9ae64e2b 100644 --- a/lib/web_ui/lib/src/engine/surface/picture.dart +++ b/lib/web_ui/lib/src/engine/surface/picture.dart @@ -100,6 +100,10 @@ class PersistedHoudiniPicture extends PersistedPicture { return existingSurface.picture == picture ? 0.0 : 1.0; } + @override + Matrix4 get localTransformInverse => + _localTransformInverse ??= Matrix4.identity(); + static void _registerCssPainter() { _cssPainterRegistered = true; final dynamic css = js_util.getProperty(html.window, 'CSS'); @@ -186,6 +190,9 @@ class PersistedStandardPicture extends PersistedPicture { } } + @override + Matrix4 get localTransformInverse => null; + @override int get bitmapPixelCount { if (_canvas is! BitmapCanvas) { @@ -358,7 +365,6 @@ abstract class PersistedPicture extends PersistedLeafSurface { _transform = _transform.clone(); _transform.translate(dx, dy); } - _globalClip = parent._globalClip; _computeExactCullRects(); } @@ -389,39 +395,52 @@ abstract class PersistedPicture extends PersistedLeafSurface { void _computeExactCullRects() { assert(transform != null); assert(localPaintBounds != null); - final ui.Rect globalPaintBounds = localClipRectToGlobalClip( - localClip: localPaintBounds, transform: transform); - // The exact cull rect required in screen coordinates. - ui.Rect tightGlobalCullRect = globalPaintBounds.intersect(_globalClip); - - // The exact cull rect required in local coordinates. - ui.Rect tightLocalCullRect; - if (tightGlobalCullRect.width <= 0 || tightGlobalCullRect.height <= 0) { - tightGlobalCullRect = ui.Rect.zero; - tightLocalCullRect = ui.Rect.zero; - } else { - final Matrix4 invertedTransform = - Matrix4.fromFloat64List(Float64List(16)); - - // TODO(yjbanov): When we move to our own vector math library, rewrite - // this to check for the case of simple transform before - // inverting. Inversion of simple transforms can be made - // much cheaper. - final double det = invertedTransform.copyInverse(transform); - if (det == 0) { - // Determinant is zero, which means the transform is not invertible. - tightGlobalCullRect = ui.Rect.zero; - tightLocalCullRect = ui.Rect.zero; - } else { - tightLocalCullRect = localClipRectToGlobalClip( - localClip: tightGlobalCullRect, transform: invertedTransform); + if (parent._projectedClip == null) { + // Compute and cache chain of clipping bounds on parent of picture since + // parent may include multiple pictures so it can be reused by all + // child pictures. + ui.Rect bounds; + PersistedSurface parentSurface = parent; + final Matrix4 clipTransform = Matrix4.identity(); + while (parentSurface != null) { + final ui.Rect localClipBounds = parentSurface._localClipBounds; + if (localClipBounds != null) { + if (bounds == null) { + bounds = transformRect(clipTransform, localClipBounds); + } else { + bounds = + bounds.intersect(transformRect(clipTransform, localClipBounds)); + } + } + final Matrix4 localInverse = parentSurface.localTransformInverse; + if (localInverse != null && !localInverse.isIdentity()) { + clipTransform.multiply(localInverse); + } + parentSurface = parentSurface.parent; } + if (bounds != null && (bounds.width <= 0 || bounds.height <= 0)) { + bounds = ui.Rect.zero; + } + // Cache projected clip on parent. + parent._projectedClip = bounds; + } + // Intersect localPaintBounds with parent projected clip to calculate + // and cache [_exactLocalCullRect]. + if (parent._projectedClip == null) { + _exactLocalCullRect = localPaintBounds; + } else { + _exactLocalCullRect = localPaintBounds.intersect(parent._projectedClip); + } + if (_exactLocalCullRect.width <= 0 || _exactLocalCullRect.height <= 0) { + _exactLocalCullRect = ui.Rect.zero; + _exactGlobalCullRect = ui.Rect.zero; + } else { + assert(() { + _exactGlobalCullRect = transformRect(transform, _exactLocalCullRect); + return true; + }()); } - - assert(tightLocalCullRect != null); - _exactLocalCullRect = tightLocalCullRect; - _exactGlobalCullRect = tightGlobalCullRect; } bool _computeOptimalCullRect(PersistedPicture oldSurface) { diff --git a/lib/web_ui/lib/src/engine/surface/platform_view.dart b/lib/web_ui/lib/src/engine/surface/platform_view.dart index ed470233b930790ba24a87d11fe94d32bbccc78b..69391c6471e24fbd4386538e8ae758add04cfb44 100644 --- a/lib/web_ui/lib/src/engine/surface/platform_view.dart +++ b/lib/web_ui/lib/src/engine/surface/platform_view.dart @@ -53,6 +53,9 @@ class PersistedPlatformView extends PersistedLeafSurface { return _hostElement; } + @override + Matrix4 get localTransformInverse => null; + @override void apply() { _hostElement.style diff --git a/lib/web_ui/lib/src/engine/surface/scene.dart b/lib/web_ui/lib/src/engine/surface/scene.dart index 5d396a63aa89593b279f5bc10cfd0f10f340a1d5..b9c4599e54ef78adc7d97b4f30ec60ea3fd11c8e 100644 --- a/lib/web_ui/lib/src/engine/surface/scene.dart +++ b/lib/web_ui/lib/src/engine/surface/scene.dart @@ -18,9 +18,14 @@ class PersistedScene extends PersistedContainerSurface { // update this code when we add add2app support. final double screenWidth = html.window.innerWidth.toDouble(); final double screenHeight = html.window.innerHeight.toDouble(); - _globalClip = ui.Rect.fromLTRB(0, 0, screenWidth, screenHeight); + _localClipBounds = ui.Rect.fromLTRB(0, 0, screenWidth, screenHeight); + _localTransformInverse = Matrix4.identity(); + _projectedClip = null; } + @override + Matrix4 get localTransformInverse => _localTransformInverse; + @override html.Element createElement() { return defaultCreateElement('flt-scene'); diff --git a/lib/web_ui/lib/src/engine/surface/surface.dart b/lib/web_ui/lib/src/engine/surface/surface.dart index da75a9c5991f5c9add3d54dfc6d8550c3e5ef362..0dccf7cffc181ec086261d57c42d73bac654f617 100644 --- a/lib/web_ui/lib/src/engine/surface/surface.dart +++ b/lib/web_ui/lib/src/engine/surface/surface.dart @@ -799,8 +799,15 @@ abstract class PersistedSurface implements ui.EngineLayer { /// the clip added by this layer (if any). /// /// The value is update by [recomputeTransformAndClip]. - ui.Rect get globalClip => _globalClip; - ui.Rect _globalClip; + ui.Rect _projectedClip; + + /// Bounds of clipping performed by this layer. + ui.Rect _localClipBounds; + // Cached inverse of transform on this node. Unlike transform, this + // Matrix only contains local transform (not chain multiplied since root). + Matrix4 _localTransformInverse; + + Matrix4 get localTransformInverse; /// Recomputes [transform] and [globalClip] fields. /// @@ -812,7 +819,9 @@ abstract class PersistedSurface implements ui.EngineLayer { @protected void recomputeTransformAndClip() { _transform = parent._transform; - _globalClip = parent._globalClip; + _localClipBounds = null; + _localTransformInverse = null; + _projectedClip = null; } /// Performs computations before [build], [update], or [retain] are called. @@ -925,7 +934,9 @@ abstract class PersistedContainerSurface extends PersistedSurface { @override void recomputeTransformAndClip() { _transform = parent._transform; - _globalClip = parent._globalClip; + _localClipBounds = null; + _localTransformInverse = null; + _projectedClip = null; } @override diff --git a/lib/web_ui/lib/src/engine/surface/transform.dart b/lib/web_ui/lib/src/engine/surface/transform.dart index abf6f1ff8c52b31f325034e127b5c253ae2298d3..021215e5114bc18cf8ee94fe19054c8022e49f74 100644 --- a/lib/web_ui/lib/src/engine/surface/transform.dart +++ b/lib/web_ui/lib/src/engine/surface/transform.dart @@ -15,7 +15,15 @@ class PersistedTransform extends PersistedContainerSurface @override void recomputeTransformAndClip() { _transform = parent._transform.multiplied(Matrix4.fromFloat64List(matrix4)); - _globalClip = parent._globalClip; + _localTransformInverse = null; + _projectedClip = null; + } + + @override + Matrix4 get localTransformInverse { + _localTransformInverse ??= + Matrix4.tryInvert(Matrix4.fromFloat64List(matrix4)); + return _localTransformInverse; } @override diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index c947860c2bceae83f955c1df85738aa0fe1ef62e..4a067d06f7a8cbf8872659dbfce9aaeb497aff7e 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -82,12 +82,30 @@ class RulerManager { _rulerHost?.remove(); } + // Evicts all rulers from the cache. + void _evictAllRulers() { + _rulers.forEach((ParagraphGeometricStyle style, ParagraphRuler ruler) { + ruler.dispose(); + }); + _rulers = {}; + } + + /// If [window._isPhysicalSizeActuallyEmpty], evicts all rulers from the cache. /// If ruler cache size exceeds [rulerCacheCapacity], evicts those rulers that /// were used the least. /// /// Resets hit counts back to zero. @visibleForTesting void cleanUpRulerCache() { + // Measurements performed (and cached) inside a hidden iframe (with + // display:none) are wrong. + // Evict all rulers, so text gets re-measured when the iframe becomes + // visible. + // see: https://github.com/flutter/flutter/issues/36341 + if (window.physicalSize.isEmpty) { + _evictAllRulers(); + return; + } if (_rulers.length > rulerCacheCapacity) { final List sortedByUsage = _rulers.values.toList(); sortedByUsage.sort((ParagraphRuler a, ParagraphRuler b) { @@ -174,7 +192,13 @@ abstract class TextMeasurementService { // TODO(flutter_web): https://github.com/flutter/flutter/issues/33523 // When the canvas-based implementation is complete and passes all the // tests, get rid of [_experimentalEnableCanvasImplementation]. - if (enableExperimentalCanvasImplementation && + // We need to check [window.physicalSize.isEmpty] because some canvas + // commands don't work as expected when they run inside a hidden iframe + // (with display:none) + // Skip using canvas measurements until the iframe becomes visible. + // see: https://github.com/flutter/flutter/issues/36341 + if (!window.physicalSize.isEmpty && + enableExperimentalCanvasImplementation && _canUseCanvasMeasurement(paragraph)) { return canvasInstance; } @@ -564,7 +588,7 @@ class CanvasTextMeasurementService extends TextMeasurementService { ui.TextPosition getTextPositionForOffset(EngineParagraph paragraph, ui.ParagraphConstraints constraints, ui.Offset offset) { // TODO(flutter_web): implement. - return new ui.TextPosition(offset: 0); + return const ui.TextPosition(offset: 0); } } diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index dba71fe15edd1b5f0f2359bf2f7b0c393c56b484..86c5ed5c43b823e5114f1f576f6a9019062cac71 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -957,7 +957,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder { final dynamic op = _ops[i]; if (op is EngineTextStyle) { final html.SpanElement span = domRenderer.createElement('span'); - _applyTextStyleToElement(element: span, style: op); + _applyTextStyleToElement(element: span, style: op, isSpan: true); if (op._background != null) { _applyTextBackgroundToElement(element: span, style: op); } @@ -997,8 +997,11 @@ String fontWeightToCss(ui.FontWeight fontWeight) { if (fontWeight == null) { return null; } + return fontWeightIndexToCss(fontWeightIndex: fontWeight.index); +} - switch (fontWeight.index) { +String fontWeightIndexToCss({int fontWeightIndex = 3}) { + switch (fontWeightIndex) { case 0: return '100'; case 1: @@ -1021,7 +1024,7 @@ String fontWeightToCss(ui.FontWeight fontWeight) { assert(() { throw AssertionError( - 'Failed to convert font weight $fontWeight to CSS.', + 'Failed to convert font weight $fontWeightIndex to CSS.', ); }()); @@ -1043,7 +1046,7 @@ void _applyParagraphStyleToElement({ final html.CssStyleDeclaration cssStyle = element.style; if (previousStyle == null) { if (style._textAlign != null) { - cssStyle.textAlign = _textAlignToCssValue( + cssStyle.textAlign = textAlignToCssValue( style._textAlign, style._textDirection ?? ui.TextDirection.ltr); } if (style._lineHeight != null) { @@ -1067,7 +1070,7 @@ void _applyParagraphStyleToElement({ } } else { if (style._textAlign != previousStyle._textAlign) { - cssStyle.textAlign = _textAlignToCssValue( + cssStyle.textAlign = textAlignToCssValue( style._textAlign, style._textDirection ?? ui.TextDirection.ltr); } if (style._lineHeight != style._lineHeight) { @@ -1098,10 +1101,13 @@ void _applyParagraphStyleToElement({ /// corresponding CSS equivalents. /// /// If [previousStyle] is not null, updates only the mismatching attributes. +/// If [isSpan] is true, the text element is a span within richtext and +/// should not assign effectiveFontFamily if fontFamily was not specified. void _applyTextStyleToElement({ @required html.HtmlElement element, @required EngineTextStyle style, EngineTextStyle previousStyle, + bool isSpan = false, }) { assert(element != null); assert(style != null); @@ -1122,8 +1128,16 @@ void _applyTextStyleToElement({ cssStyle.fontStyle = style._fontStyle == ui.FontStyle.normal ? 'normal' : 'italic'; } - if (style._effectiveFontFamily != null) { - cssStyle.fontFamily = style._effectiveFontFamily; + // For test environment use effectiveFontFamily since we need to + // consistently use Ahem font. + if (isSpan && !ui.debugEmulateFlutterTesterEnvironment) { + if (style._fontFamily != null) { + cssStyle.fontFamily = style._fontFamily; + } + } else { + if (style._effectiveFontFamily != null) { + cssStyle.fontFamily = style._effectiveFontFamily; + } } if (style._letterSpacing != null) { cssStyle.letterSpacing = '${style._letterSpacing}px'; @@ -1265,8 +1279,7 @@ String _textDirectionToCssValue(ui.TextDirection textDirection) { /// ```css /// text-align: right; /// ``` -String _textAlignToCssValue( - ui.TextAlign align, ui.TextDirection textDirection) { +String textAlignToCssValue(ui.TextAlign align, ui.TextDirection textDirection) { switch (align) { case ui.TextAlign.left: return 'left'; diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index 7bd18197529c904cdfbb532e6f983dd76261fddd..ab51b7cbf1fd0f8fb621c6a3ac4821f41ef55882 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -191,7 +191,14 @@ class TextDimensions { // match the style set on the `element`. Setting text as plain string is // faster because it doesn't change the DOM structure or CSS attributes, // and therefore doesn't trigger style recalculations in the browser. - _element.text = plainText; + if (plainText.endsWith('\n')) { + // On the web the last newline is ignored. To be consistent with + // native engine implementation we add extra newline to get correct + // height measurement. + _element.text = '$plainText\n'; + } else { + _element.text = plainText; + } } else { // Rich text: deeply copy contents. This is the slow case that should be // avoided if fast layout performance is desired. @@ -425,8 +432,10 @@ class ParagraphRuler { minIntrinsicDimensions._element.style ..flex = '0' ..display = 'inline' - // Preserve whitespaces. - ..whiteSpace = 'pre-wrap'; + // Preserve newlines, wrap text, remove end of line spaces. + // Not using pre-wrap here since end of line space hang measurement + // changed in Chrome 77 Beta. + ..whiteSpace = 'pre-line'; _minIntrinsicHost.append(minIntrinsicDimensions._element); rulerManager.addHostElement(_minIntrinsicHost); diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing.dart index 228e68066e3eef28f0db86e69a3a6a454b303aa3..694a3f9f6c790eb53d6cb93e2a8d59801526ef7b 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing.dart @@ -9,51 +9,38 @@ const bool _debugVisibleTextEditing = false; void _emptyCallback(dynamic _) {} -void _styleEditingElement(html.HtmlElement domElement) { +/// These style attributes are constant throughout the life time of an input +/// element. +/// +/// They are assigned once during the creation of the dom element. +void _setStaticStyleAttributes(html.HtmlElement domElement) { domElement.style - ..position = 'fixed' - ..whiteSpace = 'pre'; + ..whiteSpace = 'pre' + ..alignContent = 'center' + ..position = 'absolute' + ..padding = '0' + ..opacity = '1'; if (_debugVisibleTextEditing) { domElement.style - ..bottom = '0' - ..right = '0' - ..font = '24px sans-serif' ..color = 'purple' ..backgroundColor = 'pink'; } else { domElement.style - ..overflow = 'hidden' - ..transform = 'translate(-99999px, -99999px)' - // width and height can't be zero because then the element would stop - // receiving edits when its content is empty. - ..width = '1px' - ..height = '1px'; - } - if (browserEngine == BrowserEngine.webkit) { - // TODO(flutter_web): Remove once webkit issue of paragraphs incorrectly - // rendering (shifting up) is resolved. Temporarily force relayout - // a frame after input is created. - html.window.animationFrame.then((num _) { - domElement.style - ..position = 'absolute' - ..bottom = '0' - ..right = '0'; - }); + ..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'); } } -html.InputElement _createInputElement() { - final html.InputElement input = html.InputElement(); - _styleEditingElement(input); - return input; -} - -html.TextAreaElement _createTextAreaElement() { - final html.TextAreaElement textarea = html.TextAreaElement(); - _styleEditingElement(textarea); - return textarea; -} - /// The current text and selection state of a text field. class EditingState { EditingState({this.text, this.baseOffset = 0, this.extentOffset = 0}); @@ -213,8 +200,9 @@ class TextEditingElement { /// Creates a non-persistent [TextEditingElement]. /// /// See [TextEditingElement.persistent] to understand what persistent mode is. - TextEditingElement(); + TextEditingElement(this.owner); + final HybridTextEditing owner; bool _enabled = false; html.HtmlElement domElement; @@ -230,6 +218,26 @@ class TextEditingElement { return type; } + /// On iOS, sets the location of the input element after focusing on it. + /// + /// On iOS, keyboard causes scrolling in the UI. This scrolling does not + /// trigger an event. In order to position the input element correctly, it is + /// important we set it's final location after focusing on it (after keyboard + /// is up). + /// + /// This method is called in the end of the 'touchend' event, therefore it is + /// called after the editing state is set. + void configureInputElementForIOS() { + if (browserEngine != BrowserEngine.webkit || + operatingSystem != OperatingSystem.iOs) { + // Only relevant on Safari. + return; + } + if (domElement != null) { + owner.setStyle(domElement); + } + } + /// Enables the element so it can be used to edit text. /// /// Register [callback] so that it gets invoked whenever any change occurs in @@ -296,11 +304,11 @@ class TextEditingElement { void _initDomElement(InputConfiguration inputConfig) { switch (inputConfig.inputType) { case InputType.text: - domElement = _createInputElement(); + domElement = owner.createInputElement(); break; case InputType.multiline: - domElement = _createTextAreaElement(); + domElement = owner.createTextAreaElement(); break; default: @@ -465,9 +473,11 @@ class PersistentTextEditingElement extends TextEditingElement { /// [domElement] so the caller can insert it before calling /// [PersistentTextEditingElement.enable]. PersistentTextEditingElement( + HybridTextEditing owner, html.HtmlElement domElement, { @required html.VoidCallback onDomElementSwap, - }) : _onDomElementSwap = onDomElementSwap { + }) : _onDomElementSwap = onDomElementSwap, + super(owner) { // Make sure the dom element is of a type that we support for text editing. assert(_getTypeFromElement(domElement) != null); this.domElement = domElement; @@ -527,7 +537,12 @@ final HybridTextEditing textEditing = HybridTextEditing(); class HybridTextEditing { /// The default HTML element used to manage editing state when a custom /// element is not provided via [useCustomEditableElement]. - TextEditingElement _defaultEditingElement = TextEditingElement(); + TextEditingElement _defaultEditingElement; + + /// Private constructor so this class can be a singleton. + HybridTextEditing() { + _defaultEditingElement = TextEditingElement(this); + } /// The HTML element used to manage editing state. /// @@ -548,7 +563,7 @@ class HybridTextEditing { /// Use [stopUsingCustomEditableElement] to switch back to default element. void useCustomEditableElement(TextEditingElement customEditingElement) { if (_isEditing && customEditingElement != _customEditingElement) { - _stopEditing(); + stopEditing(); } _customEditingElement = customEditingElement; } @@ -560,7 +575,15 @@ class HybridTextEditing { } int _clientId; + + /// Flag which shows if there is an ongoing editing. + /// + /// Also used to define if a keyboard is needed. bool _isEditing = false; + + /// Flag indicating if the flutter framework requested a keyboard. + bool get needsKeyboard => _isEditing; + Map _configuration; /// All "flutter/textinput" platform messages should be sent to this method. @@ -568,6 +591,11 @@ class HybridTextEditing { final MethodCall call = const JSONMethodCodec().decodeMethodCall(data); switch (call.method) { case 'TextInput.setClient': + final bool clientIdChanged = + _clientId != null && _clientId != call.arguments[0]; + if (clientIdChanged && _isEditing) { + stopEditing(); + } _clientId = call.arguments[0]; _configuration = call.arguments[1]; break; @@ -583,10 +611,18 @@ class HybridTextEditing { } break; + case 'TextInput.setEditingLocationSize': + _setLocation(call.arguments); + break; + + case 'TextInput.setStyle': + _setFontStyle(call.arguments); + break; + case 'TextInput.clearClient': case 'TextInput.hide': if (_isEditing) { - _stopEditing(); + stopEditing(); } break; } @@ -601,12 +637,66 @@ class HybridTextEditing { ); } - void _stopEditing() { + void stopEditing() { assert(_isEditing); _isEditing = false; editingElement.disable(); } + _EditingStyle _editingStyle; + _EditingStyle get editingStyle => _editingStyle; + + /// Use the font size received from Flutter if set. + String font() { + assert(_editingStyle != null); + return '${_editingStyle.fontWeight} ${_editingStyle.fontSize}px ${_editingStyle.fontFamily}'; + } + + void _setFontStyle(Map style) { + assert(style.containsKey('fontSize')); + assert(style.containsKey('fontFamily')); + assert(style.containsKey('textAlignIndex')); + + final int textAlignIndex = style.remove('textAlignIndex'); + + /// Converts integer value coming as fontWeightValue 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'), + textAlign: ui.TextAlign.values[textAlignIndex], + fontFamily: style.remove('fontFamily'), + fontWeight: fontWeightIndexToCss( + fontWeightIndex: style.remove('fontWeightValue')), + ); + } + + /// 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; + + 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')); + + if (editingElement.domElement != null) { + _setDynamicStyleAttributes(editingElement.domElement); + } + } + void _syncEditingStateToFlutter(EditingState editingState) { ui.window.onPlatformMessage( 'flutter/textinput', @@ -619,4 +709,95 @@ class HybridTextEditing { _emptyCallback, ); } + + /// These style attributes are dynamic throughout the life time of an input + /// element. + /// + /// They are changed depending on the messages coming from method calls: + /// "TextInput.setStyle", "TextInput.setEditingLocationSize". + void _setDynamicStyleAttributes(html.HtmlElement domElement) { + if (_editingLocationAndSize != null && + !(browserEngine == BrowserEngine.webkit && + operatingSystem == OperatingSystem.iOs)) { + setStyle(domElement); + } + } + + /// Set style to the native dom element used for text editing. + /// + /// It will be located exactly in the same place with the editable widgets, + /// however it's contents and cursor will be invisible. + /// + /// 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) { + 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(); + } + } + + html.InputElement createInputElement() { + final html.InputElement input = html.InputElement(); + _setStaticStyleAttributes(input); + _setDynamicStyleAttributes(input); + return input; + } + + html.TextAreaElement createTextAreaElement() { + final html.TextAreaElement textarea = html.TextAreaElement(); + _setStaticStyleAttributes(textarea); + _setDynamicStyleAttributes(textarea); + return textarea; + } +} + +/// Information on the font and alignment of a text editing element. +/// +/// This information is received via TextInput.setStyle message. +class _EditingStyle { + _EditingStyle({ + @required this.textDirection, + @required this.fontSize, + @required this.textAlign, + @required this.fontFamily, + this.fontWeight, + }); + + /// This information will be used for changing the style of the hidden input + /// element, which will match it's size to the size of the editable widget. + final double fontSize; + final String fontWeight; + final String fontFamily; + final ui.TextAlign textAlign; + final ui.TextDirection textDirection; + + String get align => textAlignToCssValue(textAlign, textDirection); +} + +/// Information on the location and size of the editing element. +/// +/// This information is received via "TextInput.setEditingLocationSize" +/// 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; + final double width; + final double height; } diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 606fdfa12af8e0cc35f5ee8cd69eb8e363062d8d..b0ec8422d894766a89bcdccc197794dea5929fea 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -110,38 +110,25 @@ bool get assertionsEnabled { return k; } -/// Converts a rectangular clip specified in local coordinates to screen -/// coordinates given the effective [transform]. +/// Transforms a [ui.Rect] given the effective [transform]. /// -/// The resulting clip is a rectangle aligned to the pixel grid, i.e. two of +/// The resulting rect is aligned to the pixel grid, i.e. two of /// its sides are vertical and two are horizontal. In the presence of rotations /// the rectangle is inflated such that it fits the rotated rectangle. -ui.Rect localClipRectToGlobalClip({ui.Rect localClip, Matrix4 transform}) { - return localClipToGlobalClip( - localLeft: localClip.left, - localTop: localClip.top, - localRight: localClip.right, - localBottom: localClip.bottom, - transform: transform, - ); +ui.Rect transformRect(Matrix4 transform, ui.Rect rect) { + return transformLTRB(transform, rect.left, rect.top, rect.right, rect.bottom); } -/// Converts a rectangular clip specified in local coordinates to screen -/// coordinates given the effective [transform]. +/// Transforms a rectangle given the effective [transform]. /// -/// This is the same as [localClipRectToGlobalClip], except that the local clip -/// rect is specified in terms of left, top, right, and bottom edge offsets. -ui.Rect localClipToGlobalClip({ - double localLeft, - double localTop, - double localRight, - double localBottom, - Matrix4 transform, -}) { - assert(localLeft != null); - assert(localTop != null); - assert(localRight != null); - assert(localBottom != null); +/// This is the same as [transformRect], except that the rect is specified +/// in terms of left, top, right, and bottom edge offsets. +ui.Rect transformLTRB( + Matrix4 transform, double left, double top, double right, double bottom) { + assert(left != null); + assert(top != null); + assert(right != null); + assert(bottom != null); // Construct a matrix where each row represents a vector pointing at // one of the four corners of the (left, top, right, bottom) rectangle. @@ -162,23 +149,23 @@ ui.Rect localClipToGlobalClip({ final Float64List pointData = Float64List(16); // Row 0: top-left - pointData[0] = localLeft; - pointData[4] = localTop; + pointData[0] = left; + pointData[4] = top; pointData[12] = 1; // Row 1: top-right - pointData[1] = localRight; - pointData[5] = localTop; + pointData[1] = right; + pointData[5] = top; pointData[13] = 1; // Row 2: bottom-left - pointData[2] = localLeft; - pointData[6] = localBottom; + pointData[2] = left; + pointData[6] = bottom; pointData[14] = 1; // Row 3: bottom-right - pointData[3] = localRight; - pointData[7] = localBottom; + pointData[3] = right; + pointData[7] = bottom; pointData[15] = 1; final Matrix4 pointMatrix = Matrix4.fromFloat64List(pointData); @@ -232,3 +219,22 @@ String _pathToSvgClipPath(ui.Path path, sb.write('"> fontFeatures, }) = engine.EngineTextStyle; + @override int get hashCode; + @override bool operator ==(dynamic other); + @override String toString(); } @@ -551,10 +554,13 @@ abstract class ParagraphStyle { Locale locale, }) = engine.EngineParagraphStyle; + @override bool operator ==(dynamic other); + @override int get hashCode; + @override String toString(); } @@ -605,8 +611,10 @@ abstract class StrutStyle { bool forceStrutHeight, }) = engine.EngineStrutStyle; + @override int get hashCode; + @override bool operator ==(dynamic other); } diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index bc70492dbd1790e51f7297ab2dc3ccd74e60abab..567515595fc458c3a2eda1092678c98da8338eb1 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -496,8 +496,12 @@ class Locale { @override String toString() { final StringBuffer out = StringBuffer(languageCode); - if (scriptCode != null) out.write('_$scriptCode'); - if (_countryCode != null) out.write('_$countryCode'); + if (scriptCode != null) { + out.write('_$scriptCode'); + } + if (_countryCode != null) { + out.write('_$countryCode'); + } return out.toString(); } @@ -1098,17 +1102,29 @@ class AccessibilityFeatures { @override String toString() { final List features = []; - if (accessibleNavigation) features.add('accessibleNavigation'); - if (invertColors) features.add('invertColors'); - if (disableAnimations) features.add('disableAnimations'); - if (boldText) features.add('boldText'); - if (reduceMotion) features.add('reduceMotion'); + if (accessibleNavigation) { + features.add('accessibleNavigation'); + } + if (invertColors) { + features.add('invertColors'); + } + if (disableAnimations) { + features.add('disableAnimations'); + } + if (boldText) { + features.add('boldText'); + } + if (reduceMotion) { + features.add('reduceMotion'); + } return 'AccessibilityFeatures$features'; } @override bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) return false; + if (other.runtimeType != runtimeType) { + return false; + } final AccessibilityFeatures typedOther = other; return _index == typedOther._index; } diff --git a/lib/web_ui/test/compositing_test.dart b/lib/web_ui/test/compositing_test.dart index 10e449a0e2e22e89dc1fb5f2abac7f7a2085bf35..005a7f0866434069ba0b077eb9a922edea2fa98e 100644 --- a/lib/web_ui/test/compositing_test.dart +++ b/lib/web_ui/test/compositing_test.dart @@ -274,6 +274,9 @@ class MockPersistedPicture extends PersistedPicture { return identical(existingSurface.picture, picture) ? 0.0 : 1.0; } + @override + Matrix4 get localTransformInverse => null; + @override void build() { super.build(); diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index 133d30e66f79563b0d92aee4b3e48eaf1843d13f..cb9f76a72cbc779c739793e25793a993470f6b41 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -27,7 +27,7 @@ void main() { 'is after a delay', () { // Set the a11y announcement's duration on DOM to half seconds. accessibilityAnnouncements.durationA11yMessageIsOnDom = - Duration(milliseconds: 500); + const Duration(milliseconds: 500); // Initially there is no accessibility-element expect(document.getElementById('accessibility-element'), isNull); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index acadba679ed3fd57e5d37d113fce96375804f08e..62e4e334cb4322b83ddbdc61956339cb3320e696 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -42,7 +42,7 @@ void trackEditingState(EditingState editingState) { void main() { group('$TextEditingElement', () { setUp(() { - editingElement = TextEditingElement(); + editingElement = TextEditingElement(HybridTextEditing()); }); tearDown(() { @@ -193,7 +193,8 @@ void main() { // A regular shouldn't be accepted. final HtmlElement span = SpanElement(); expect( - () => PersistentTextEditingElement(span, onDomElementSwap: null), + () => PersistentTextEditingElement(HybridTextEditing(), span, + onDomElementSwap: null), throwsAssertionError, ); }); @@ -203,7 +204,8 @@ void main() { // re-acquiring focus shouldn't happen in persistent mode. final InputElement input = InputElement(); final PersistentTextEditingElement persistentEditingElement = - PersistentTextEditingElement(input, onDomElementSwap: () {}); + PersistentTextEditingElement(HybridTextEditing(), input, + onDomElementSwap: () {}); expect(document.activeElement, document.body); document.body.append(input); @@ -221,7 +223,8 @@ void main() { test('Does not dispose and recreate dom elements in persistent mode', () { final InputElement input = InputElement(); final PersistentTextEditingElement persistentEditingElement = - PersistentTextEditingElement(input, onDomElementSwap: () {}); + PersistentTextEditingElement(HybridTextEditing(), input, + onDomElementSwap: () {}); // The DOM element should've been eagerly created. expect(input, isNotNull); @@ -254,7 +257,8 @@ void main() { test('Refocuses when setting editing state', () { final InputElement input = InputElement(); final PersistentTextEditingElement persistentEditingElement = - PersistentTextEditingElement(input, onDomElementSwap: () {}); + PersistentTextEditingElement(HybridTextEditing(), input, + onDomElementSwap: () {}); document.body.append(input); persistentEditingElement.enable(singlelineConfig, @@ -274,7 +278,8 @@ void main() { test('Works in multi-line mode', () { final TextAreaElement textarea = TextAreaElement(); final PersistentTextEditingElement persistentEditingElement = - PersistentTextEditingElement(textarea, onDomElementSwap: () {}); + PersistentTextEditingElement(HybridTextEditing(), textarea, + onDomElementSwap: () {}); expect(persistentEditingElement.domElement, textarea); expect(document.activeElement, document.body); @@ -321,6 +326,7 @@ void main() { }); tearDown(() { + // TODO(mdebbar): clean-up stuff that HybridTextEditing registered on the page spy.deactivate(); }); @@ -389,6 +395,40 @@ void main() { expect(spy.messages, isEmpty); }); + test('setClient, setEditingState, show, setClient', () { + final MethodCall setClient = MethodCall( + 'TextInput.setClient', [123, flutterSinglelineConfig]); + textEditing.handleTextInput(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + textEditing.handleTextInput(codec.encodeMethodCall(setEditingState)); + + // Editing shouldn't have started yet. + expect(document.activeElement, document.body); + + const MethodCall show = MethodCall('TextInput.show'); + textEditing.handleTextInput(codec.encodeMethodCall(show)); + + checkInputEditingState( + textEditing.editingElement.domElement, 'abcd', 2, 3); + + final MethodCall setClient2 = MethodCall( + 'TextInput.setClient', [567, flutterSinglelineConfig]); + textEditing.handleTextInput(codec.encodeMethodCall(setClient2)); + + // Receiving another client via setClient should stop editing, hence + // should remove the previous active element. + expect(document.activeElement, document.body); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + }); + test('setClient, setEditingState, show, setEditingState, clearClient', () { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); @@ -424,6 +464,60 @@ void main() { expect(spy.messages, isEmpty); }); + test( + 'setClient, setLocationSize, setStyle, setEditingState, show, clearClient', + () { + final MethodCall setClient = MethodCall( + 'TextInput.setClient', [123, flutterSinglelineConfig]); + textEditing.handleTextInput(codec.encodeMethodCall(setClient)); + + const MethodCall setLocationSize = + MethodCall('TextInput.setEditingLocationSize', { + 'top': 0, + 'left': 0, + 'width': 150, + 'height': 50, + }); + textEditing.handleTextInput(codec.encodeMethodCall(setLocationSize)); + + const MethodCall setStyle = + MethodCall('TextInput.setStyle', { + 'fontSize': 12, + 'fontFamily': 'sans-serif', + 'textAlignIndex': 4, + 'fontWeightValue': 4, + }); + textEditing.handleTextInput(codec.encodeMethodCall(setStyle)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + textEditing.handleTextInput(codec.encodeMethodCall(setEditingState)); + + const MethodCall show = MethodCall('TextInput.show'); + textEditing.handleTextInput(codec.encodeMethodCall(show)); + + checkInputEditingState( + textEditing.editingElement.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))); + expect(textEditing.editingElement.domElement.style.font, + '500 12px sans-serif'); + + const MethodCall clearClient = MethodCall('TextInput.clearClient'); + textEditing.handleTextInput(codec.encodeMethodCall(clearClient)); + + // Confirm that [HybridTextEditing] didn't send any messages. + expect(spy.messages, isEmpty); + }); + test('Syncs the editing state back to Flutter', () { final MethodCall setClient = MethodCall( 'TextInput.setClient', [123, flutterSinglelineConfig]); diff --git a/lib/web_ui/test/text_test.dart b/lib/web_ui/test/text_test.dart index 60a891acd26f26c7f35014fe82acce46605cf8c9..eab005177c5fc933c3083c73756046857d03d71a 100644 --- a/lib/web_ui/test/text_test.dart +++ b/lib/web_ui/test/text_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:html'; + import 'package:test/test.dart'; import 'package:ui/ui.dart'; @@ -216,4 +218,31 @@ void main() async { expect(paragraph.getPositionForOffset(const Offset(150, 0)).offset, thirdSpanStartPosition); }); + + // Regression test for https://github.com/flutter/flutter/issues/38972 + test( + 'should not set fontFamily to effectiveFontFamily for spans in rich text', + () { + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + fontFamily: 'Roboto', + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + fontSize: 15.0, + )); + builder + .pushStyle(TextStyle(fontFamily: 'Menlo', fontWeight: FontWeight.bold)); + const String firstSpanText = 'abc'; + builder.addText(firstSpanText); + builder.pushStyle(TextStyle(fontSize: 30.0, fontWeight: FontWeight.normal)); + const String secondSpanText = 'def'; + builder.addText(secondSpanText); + final EngineParagraph paragraph = builder.build(); + paragraph.layout(const ParagraphConstraints(width: 800.0)); + expect(paragraph.plainText, isNull); + final List spans = + paragraph.paragraphElement.querySelectorAll('span'); + expect(spans[0].style.fontFamily, 'Ahem'); + // The nested span here should not set it's family to default sans-serif. + expect(spans[1].style.fontFamily, 'Ahem'); + }); } diff --git a/lib/web_ui/tool/unicode_sync_script.dart b/lib/web_ui/tool/unicode_sync_script.dart index 04a113794ea66abc281b50934a768b93955af5cd..fdb8b30a1c9e2c6dfbe6481f2a69e2297fbf34aa 100644 --- a/lib/web_ui/tool/unicode_sync_script.dart +++ b/lib/web_ui/tool/unicode_sync_script.dart @@ -68,7 +68,7 @@ void main(List arguments) async { final String propertiesFile = arguments[0]; final String codegenFile = path.join( path.dirname(Platform.script.toFilePath()), - '../lib/src/text/word_break_properties.dart', + '../lib/src/engine/text/word_break_properties.dart', ); WordBreakPropertiesSyncer(propertiesFile, codegenFile).perform(); } @@ -103,13 +103,17 @@ class WordBreakPropertiesSyncer extends PropertiesSyncer { @override String template(List header, List data) { return ''' +// Copyright 2013 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. + // AUTO-GENERATED FILE. -// Generated by: bin/unicode_sync_script.dart +// Generated by: tool/unicode_sync_script.dart // // Source: // ${header.join('\n// ')} -import 'unicode_range.dart'; +part of engine; CharProperty getCharProperty(String text, int index) { if (index < 0 || index >= text.length) { @@ -123,7 +127,7 @@ enum CharProperty { } const UnicodePropertyLookup lookup = - UnicodePropertyLookup([ + UnicodePropertyLookup(>[ ${getLookupEntries(data).join(',\n ')} ]); '''; @@ -150,17 +154,23 @@ const UnicodePropertyLookup lookup = String generateLookupEntry(PropertyTuple tuple) { final String propertyStr = 'CharProperty.${normalizePropertyName(tuple.property)}'; - return 'UnicodeRange(${toHex(tuple.start)}, ${toHex(tuple.end)}, $propertyStr)'; + return 'UnicodeRange(${toHex(tuple.start)}, ${toHex(tuple.end)}, $propertyStr)'; } } /// Example: -/// UnicodeRange(0x01C4, 0x0293, CharProperty.ALetter), -/// UnicodeRange(0x0294, 0x0294, CharProperty.ALetter), -/// UnicodeRange(0x0295, 0x02AF, CharProperty.ALetter), +/// +/// ``` +/// UnicodeRange(0x01C4, 0x0293, CharProperty.ALetter), +/// UnicodeRange(0x0294, 0x0294, CharProperty.ALetter), +/// UnicodeRange(0x0295, 0x02AF, CharProperty.ALetter), +/// ``` /// /// will get combined into: -/// UnicodeRange(0x01C4, 0x02AF, CharProperty.ALetter) +/// +/// ``` +/// UnicodeRange(0x01C4, 0x02AF, CharProperty.ALetter) +/// ``` List combineAdjacentRanges(List data) { final List result = [data.first]; for (int i = 1; i < data.length; i++) {