未验证 提交 f38913b7 编写于 作者: Y Yegor 提交者: GitHub

sync Flutter Web engine to the latest (#11421)

* sync Flutter Web engine to the latest
上级 82fcf325
......@@ -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;
......
......@@ -38,3 +38,57 @@ BrowserEngine _detectBrowserEngine() {
return BrowserEngine.unknown;
}
/// Operating system where the current browser runs.
///
/// Taken from the navigator platform.
/// <https://developer.mozilla.org/en-US/docs/Web/API/NavigatorID/platform>
enum OperatingSystem {
/// iOS: <http://www.apple.com/ios/>
iOs,
/// Android: <https://www.android.com/>
android,
/// Linux: <https://www.linux.org/>
linux,
/// Windows: <https://www.microsoft.com/windows/>
windows,
/// MacOs: <https://www.apple.com/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;
}
}
// 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 {
......
// 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.
......
......@@ -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();
}
}
......
// 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<double> makeSkPoint(ui.Offset point) {
js.JsArray<double> skPoint = new js.JsArray<double>();
final js.JsArray<double> skPoint = js.JsArray<double>();
skPoint.length = 2;
skPoint[0] = point.dx;
skPoint[1] = point.dy;
......
......@@ -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) {
......
......@@ -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<String> 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<String> defaultStackFilter(Iterable<String> frames) {
result.add('(elided one frame from ${skipped.single})');
} else if (skipped.length > 1) {
final List<String> where = Set<String>.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 {
......
......@@ -56,7 +56,7 @@ void handlePlatformViewCall(
void _createPlatformView(
MethodCall methodCall, ui.PlatformMessageResponseCallback callback) {
final Map args = methodCall.arguments;
final Map<dynamic, dynamic> args = methodCall.arguments;
final int id = args['id'];
final String viewType = args['viewType'];
// TODO(het): Use 'direction', 'width', and 'height'.
......
......@@ -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) {
......
......@@ -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;
......
......@@ -20,6 +20,7 @@ class TextField extends RoleManager {
? html.TextAreaElement()
: html.InputElement();
persistentTextEditingElement = PersistentTextEditingElement(
textEditing,
editableDomElement,
onDomElementSwap: _setupDomElement,
);
......
......@@ -253,8 +253,10 @@ abstract class _TypedDataBuffer<E> extends ListBase<E> {
///
/// 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<E> newBuffer = _createBiggerBuffer(requiredCapacity);
newBuffer.setRange(0, _length, _buffer);
_buffer = newBuffer;
}
......
......@@ -271,7 +271,9 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
@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<dynamic> {
@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<dynamic> {
writeValue(buffer, value);
});
} else {
throw new ArgumentError.value(value);
throw ArgumentError.value(value);
}
}
......@@ -359,7 +365,9 @@ class StandardMessageCodec implements MessageCodec<dynamic> {
/// 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);
......
// 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<num> jsColors = js.JsArray<num>();
final js.JsArray<num> jsColors = js.JsArray<num>();
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();
}
......
......@@ -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'
......
......@@ -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 {
......
......@@ -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';
......
......@@ -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';
......
......@@ -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) {
......
......@@ -53,6 +53,9 @@ class PersistedPlatformView extends PersistedLeafSurface {
return _hostElement;
}
@override
Matrix4 get localTransformInverse => null;
@override
void apply() {
_hostElement.style
......
......@@ -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');
......
......@@ -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
......
......@@ -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
......
......@@ -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 = <ParagraphGeometricStyle, ParagraphRuler>{};
}
/// 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<ParagraphRuler> 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);
}
}
......
......@@ -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';
......
......@@ -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);
......
......@@ -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<String, dynamic> _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<String, dynamic> 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<String, dynamic> 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;
}
......@@ -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('"></path></clipPath></defs></svg');
return sb.toString();
}
/// Determines if the (dynamic) exception passed in is a NS_ERROR_FAILURE
/// (from Firefox).
///
/// NS_ERROR_FAILURE (0x80004005) is the most general of all the (Firefox)
/// errors and occurs for all errors for which a more specific error code does
/// not apply. (https://developer.mozilla.org/en-US/docs/Mozilla/Errors)
///
/// Other browsers do not throw this exception.
///
/// In Flutter, this exception happens when we try to perform some operations on
/// a Canvas when the application is rendered in a display:none iframe.
///
/// We need this in [BitmapCanvas] and [RecordingCanvas] to swallow this
/// Firefox exception without interfering with others (potentially useful
/// for the programmer).
bool _isNsErrorFailureException(dynamic e) {
return js_util.getProperty(e, 'name') == 'NS_ERROR_FAILURE';
}
......@@ -4,6 +4,7 @@
part of ui;
// ignore: unused_element, Used in Shader assert.
bool _offsetIsValid(Offset offset) {
assert(offset != null, 'Offset argument was null.');
assert(!offset.dx.isNaN && !offset.dy.isNaN,
......@@ -11,6 +12,7 @@ bool _offsetIsValid(Offset offset) {
return true;
}
// ignore: unused_element, Used in Shader assert.
bool _matrix4IsValid(Float64List matrix4) {
assert(matrix4 != null, 'Matrix4 argument was null.');
assert(matrix4.length == 16, 'Matrix4 must have 16 entries.');
......@@ -1430,7 +1432,9 @@ class ColorFilter {
@override
bool operator ==(dynamic other) {
if (other is! ColorFilter) return false;
if (other is! ColorFilter) {
return false;
}
final ColorFilter typedOther = other;
return _color == typedOther._color && _blendMode == typedOther._blendMode;
}
......
......@@ -473,10 +473,13 @@ abstract class TextStyle {
List<FontFeature> 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);
}
......
......@@ -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<String> features = <String>[];
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;
}
......
......@@ -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();
......
......@@ -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);
......
......@@ -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 <span> 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', <dynamic>[123, flutterSinglelineConfig]);
textEditing.handleTextInput(codec.encodeMethodCall(setClient));
const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'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', <dynamic>[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', <dynamic>[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', <dynamic>[123, flutterSinglelineConfig]);
textEditing.handleTextInput(codec.encodeMethodCall(setClient));
const MethodCall setLocationSize =
MethodCall('TextInput.setEditingLocationSize', <String, dynamic>{
'top': 0,
'left': 0,
'width': 150,
'height': 50,
});
textEditing.handleTextInput(codec.encodeMethodCall(setLocationSize));
const MethodCall setStyle =
MethodCall('TextInput.setStyle', <String, dynamic>{
'fontSize': 12,
'fontFamily': 'sans-serif',
'textAlignIndex': 4,
'fontWeightValue': 4,
});
textEditing.handleTextInput(codec.encodeMethodCall(setStyle));
const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'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<double>.fromPoints(
const Point<double>(0.0, 0.0), const Point<double>(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', <dynamic>[123, flutterSinglelineConfig]);
......
......@@ -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<SpanElement> 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');
});
}
......@@ -68,7 +68,7 @@ void main(List<String> 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<String> header, List<PropertyTuple> 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<CharProperty> lookup =
UnicodePropertyLookup<CharProperty>([
UnicodePropertyLookup<CharProperty>(<UnicodeRange<CharProperty>>[
${getLookupEntries(data).join(',\n ')}
]);
''';
......@@ -150,17 +154,23 @@ const UnicodePropertyLookup<CharProperty> lookup =
String generateLookupEntry(PropertyTuple tuple) {
final String propertyStr =
'CharProperty.${normalizePropertyName(tuple.property)}';
return 'UnicodeRange(${toHex(tuple.start)}, ${toHex(tuple.end)}, $propertyStr)';
return 'UnicodeRange<CharProperty>(${toHex(tuple.start)}, ${toHex(tuple.end)}, $propertyStr)';
}
}
/// Example:
/// UnicodeRange(0x01C4, 0x0293, CharProperty.ALetter),
/// UnicodeRange(0x0294, 0x0294, CharProperty.ALetter),
/// UnicodeRange(0x0295, 0x02AF, CharProperty.ALetter),
///
/// ```
/// UnicodeRange<CharProperty>(0x01C4, 0x0293, CharProperty.ALetter),
/// UnicodeRange<CharProperty>(0x0294, 0x0294, CharProperty.ALetter),
/// UnicodeRange<CharProperty>(0x0295, 0x02AF, CharProperty.ALetter),
/// ```
///
/// will get combined into:
/// UnicodeRange(0x01C4, 0x02AF, CharProperty.ALetter)
///
/// ```
/// UnicodeRange<CharProperty>(0x01C4, 0x02AF, CharProperty.ALetter)
/// ```
List<PropertyTuple> combineAdjacentRanges(List<PropertyTuple> data) {
final List<PropertyTuple> result = <PropertyTuple>[data.first];
for (int i = 1; i < data.length; i++) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册