diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index c2a8ee725502c..1f029bb647d73 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -495,6 +495,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/web_experiments.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/annotations.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/canvas.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 00b90d9bb4b9d..04e688ff090e7 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -117,6 +117,7 @@ part 'engine/text_editing/text_editing.dart'; part 'engine/util.dart'; part 'engine/validators.dart'; part 'engine/vector_math.dart'; +part 'engine/web_experiments.dart'; part 'engine/window.dart'; bool _engineInitialized = false; @@ -161,6 +162,8 @@ void webOnlyInitializeEngine() { // initialize framework bindings. domRenderer; + WebExperiments.ensureInitialized(); + bool waitingForAnimation = false; ui.webOnlyScheduleFrameCallback = () { // We're asked to schedule a frame and call `frameHandler` when the frame diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index 3781ff6412ef5..30ae994538c1a 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -187,13 +187,6 @@ abstract class TextMeasurementService { static TextMeasurementService get canvasInstance => CanvasTextMeasurementService.instance; - /// Whether the new experimental implementation of canvas-based text - /// measurement is enabled or not. - /// - /// This is only used for testing at the moment. Once the implementation is - /// complete and production-ready, we'll get rid of this flag. - static bool enableExperimentalCanvasImplementation = const bool.fromEnvironment('FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT', defaultValue: false); - /// Gets the appropriate [TextMeasurementService] instance for the given /// [paragraph]. static TextMeasurementService forParagraph(ui.Paragraph paragraph) { @@ -206,7 +199,7 @@ abstract class TextMeasurementService { // Skip using canvas measurements until the iframe becomes visible. // see: https://github.com/flutter/flutter/issues/36341 if (!window.physicalSize.isEmpty && - enableExperimentalCanvasImplementation && + WebExperiments.instance.useCanvasText && _canUseCanvasMeasurement(paragraph)) { return canvasInstance; } diff --git a/lib/web_ui/lib/src/engine/web_experiments.dart b/lib/web_ui/lib/src/engine/web_experiments.dart new file mode 100644 index 0000000000000..2e2050938e8df --- /dev/null +++ b/lib/web_ui/lib/src/engine/web_experiments.dart @@ -0,0 +1,53 @@ +// 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. + +// @dart = 2.6 +part of engine; + +/// A bag of all experiment flags in the web engine. +/// +/// This class also handles platform messages that can be sent to enable/disable +/// certain experiments at runtime without the need to access engine internals. +class WebExperiments { + WebExperiments._() { + js.context['_flutter_internal_update_experiment'] = updateExperiment; + registerHotRestartListener(() { + js.context['_flutter_internal_update_experiment'] = null; + }); + } + + static WebExperiments ensureInitialized() { + if (WebExperiments.instance == null) { + WebExperiments.instance = WebExperiments._(); + } + return WebExperiments.instance; + } + + static WebExperiments instance; + + /// Experiment flag for using canvas-based text measurement. + bool get useCanvasText => _useCanvasText ?? false; + set useCanvasText(bool enabled) { + _useCanvasText = enabled; + } + + bool _useCanvasText = const bool.fromEnvironment( + 'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT', + defaultValue: null, + ); + + /// Reset all experimental flags to their default values. + void reset() { + _useCanvasText = null; + } + + /// Used to enable/disable experimental flags in the web engine. + void updateExperiment(String name, bool enabled) { + switch (name) { + case 'useCanvasText': + _useCanvasText = enabled; + break; + } + } +} diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 363204a120d6e..3a63ee87085d7 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -11,6 +11,10 @@ import 'package:test/test.dart'; import 'mock_engine_canvas.dart'; void main() { + setUpAll(() { + WebExperiments.ensureInitialized(); + }); + group('EngineCanvas', () { MockEngineCanvas mockCanvas; ui.Paragraph paragraph; diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart new file mode 100644 index 0000000000000..034cbba04b945 --- /dev/null +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -0,0 +1,80 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; +import 'dart:js_util' as js_util; + +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + +void main() { + setUp(() { + WebExperiments.ensureInitialized(); + }); + + tearDown(() { + WebExperiments.instance.reset(); + }); + + test('default web experiment values', () { + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('can turn on/off web experiments', () { + WebExperiments.instance.updateExperiment('useCanvasText', true); + expect(WebExperiments.instance.useCanvasText, true); + + WebExperiments.instance.updateExperiment('useCanvasText', false); + expect(WebExperiments.instance.useCanvasText, false); + + WebExperiments.instance.updateExperiment('useCanvasText', null); + // Goes back to default value. + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('ignores unknown experiments', () { + expect(WebExperiments.instance.useCanvasText, false); + WebExperiments.instance.updateExperiment('foobarbazqux', true); + expect(WebExperiments.instance.useCanvasText, false); + WebExperiments.instance.updateExperiment('foobarbazqux', false); + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('can reset web experiments', () { + WebExperiments.instance.updateExperiment('useCanvasText', true); + WebExperiments.instance.reset(); + expect(WebExperiments.instance.useCanvasText, false); + + WebExperiments.instance.updateExperiment('useCanvasText', true); + WebExperiments.instance.updateExperiment('foobarbazqux', true); + WebExperiments.instance.reset(); + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('js interop also works', () { + expect(WebExperiments.instance.useCanvasText, false); + + expect(() => jsUpdateExperiment('useCanvasText', true), returnsNormally); + expect(WebExperiments.instance.useCanvasText, true); + + expect(() => jsUpdateExperiment('useCanvasText', null), returnsNormally); + expect(WebExperiments.instance.useCanvasText, false); + }); + + test('js interop throws on wrong type', () { + expect(() => jsUpdateExperiment(123, true), throwsA(anything)); + expect(() => jsUpdateExperiment('foo', 123), throwsA(anything)); + expect(() => jsUpdateExperiment('foo', 'bar'), throwsA(anything)); + expect(() => jsUpdateExperiment(false, 'foo'), throwsA(anything)); + }); +} + +void jsUpdateExperiment(dynamic name, dynamic enabled) { + js_util.callMethod( + html.window, + '_flutter_internal_update_experiment', + [name, enabled], + ); +} diff --git a/lib/web_ui/test/golden_tests/engine/scuba.dart b/lib/web_ui/test/golden_tests/engine/scuba.dart index 421f4ae758be6..6ae5739577be5 100644 --- a/lib/web_ui/test/golden_tests/engine/scuba.dart +++ b/lib/web_ui/test/golden_tests/engine/scuba.dart @@ -71,7 +71,7 @@ class EngineScubaTester { sceneElement.append(canvas.rootElement); html.document.body.append(sceneElement); String screenshotName = '${fileName}_${canvas.runtimeType}'; - if (TextMeasurementService.enableExperimentalCanvasImplementation) { + if (WebExperiments.instance.useCanvasText) { screenshotName += '+canvas_measurement'; } await diffScreenshot( @@ -96,18 +96,20 @@ void testEachCanvas(String description, CanvasTest body, test('$description (bitmap)', () { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); + WebExperiments.instance.useCanvasText = false; return body(BitmapCanvas(bounds)); } finally { + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }); test('$description (bitmap + canvas measurement)', () async { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; await body(BitmapCanvas(bounds)); } finally { - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }); diff --git a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart index 167921f337f5b..507be4fd33df9 100644 --- a/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart @@ -5,7 +5,7 @@ // @dart = 2.6 import 'dart:async'; -import 'package:ui/ui.dart'; +import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; import 'scuba.dart'; @@ -89,7 +89,7 @@ void main() async { offset = offset.translate(0, p.height + 10); // Only the first line is rendered with an ellipsis. - if (!TextMeasurementService.enableExperimentalCanvasImplementation) { + if (!WebExperiments.instance.useCanvasText) { // This is now correct with the canvas-based measurement, so we shouldn't // print the "(wrong)" warning. p = warning('(wrong)'); @@ -106,7 +106,7 @@ void main() async { // Only the first two lines are rendered and the ellipsis appears on the 2nd // line. - if (!TextMeasurementService.enableExperimentalCanvasImplementation) { + if (!WebExperiments.instance.useCanvasText) { // This is now correct with the canvas-based measurement, so we shouldn't // print the "(wrong)" warning. p = warning('(wrong)'); diff --git a/lib/web_ui/test/paragraph_builder_test.dart b/lib/web_ui/test/paragraph_builder_test.dart index 08ac0d778fbf2..54a3bcf53b046 100644 --- a/lib/web_ui/test/paragraph_builder_test.dart +++ b/lib/web_ui/test/paragraph_builder_test.dart @@ -3,11 +3,16 @@ // found in the LICENSE file. // @dart = 2.6 +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; import 'package:test/test.dart'; void main() { + setUpAll(() { + WebExperiments.ensureInitialized(); + }); + test('Should be able to build and layout a paragraph', () { final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); builder.addText('Hello'); diff --git a/lib/web_ui/test/paragraph_test.dart b/lib/web_ui/test/paragraph_test.dart index 29d52b830218f..c3432a00bed1f 100644 --- a/lib/web_ui/test/paragraph_test.dart +++ b/lib/web_ui/test/paragraph_test.dart @@ -4,7 +4,7 @@ // @dart = 2.6 import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart'; +import 'package:ui/ui.dart' hide window; import 'package:test/test.dart'; @@ -12,18 +12,20 @@ void testEachMeasurement(String description, VoidCallback body, {bool skip}) { test('$description (dom measurement)', () async { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); + WebExperiments.instance.useCanvasText = false; return body(); } finally { + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }, skip: skip); test('$description (canvas measurement)', () async { try { TextMeasurementService.initialize(rulerCacheCapacity: 2); - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; return body(); } finally { - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; TextMeasurementService.clearCache(); } }, skip: skip); @@ -184,7 +186,7 @@ void main() async { test('getPositionForOffset multi-line', () { // [Paragraph.getPositionForOffset] for multi-line text doesn't work well // with dom-based measurement. - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -280,11 +282,11 @@ void main() async { ); TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); test('getPositionForOffset multi-line centered', () { - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -387,7 +389,7 @@ void main() async { ); TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); testEachMeasurement('getBoxesForRange returns a box', () { @@ -782,7 +784,7 @@ void main() async { test('longestLine', () { // [Paragraph.longestLine] is only supported by canvas-based measurement. - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -797,7 +799,7 @@ void main() async { expect(paragraph.longestLine, 50.0); TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); testEachMeasurement('getLineBoundary (single-line)', () { @@ -824,7 +826,7 @@ void main() async { test('getLineBoundary (multi-line)', () { // [Paragraph.getLineBoundary] for multi-line paragraphs is only supported // by canvas-based measurement. - TextMeasurementService.enableExperimentalCanvasImplementation = true; + WebExperiments.instance.useCanvasText = true; TextMeasurementService.initialize(rulerCacheCapacity: 2); final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( @@ -867,7 +869,7 @@ void main() async { } TextMeasurementService.clearCache(); - TextMeasurementService.enableExperimentalCanvasImplementation = false; + WebExperiments.instance.useCanvasText = null; }); testEachMeasurement('width should be a whole integer', () {