Skip to content

Commit 4c41b14

Browse files
authored
[web] Introduce js interop to enable experimental flags on web (flutter#17099)
1 parent 21f5d7f commit 4c41b14

File tree

10 files changed

+168
-25
lines changed

10 files changed

+168
-25
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
495495
FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart
496496
FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart
497497
FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart
498+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/web_experiments.dart
498499
FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart
499500
FILE: ../../../flutter/lib/web_ui/lib/src/ui/annotations.dart
500501
FILE: ../../../flutter/lib/web_ui/lib/src/ui/canvas.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ part 'engine/text_editing/text_editing.dart';
117117
part 'engine/util.dart';
118118
part 'engine/validators.dart';
119119
part 'engine/vector_math.dart';
120+
part 'engine/web_experiments.dart';
120121
part 'engine/window.dart';
121122

122123
bool _engineInitialized = false;
@@ -161,6 +162,8 @@ void webOnlyInitializeEngine() {
161162
// initialize framework bindings.
162163
domRenderer;
163164

165+
WebExperiments.ensureInitialized();
166+
164167
bool waitingForAnimation = false;
165168
ui.webOnlyScheduleFrameCallback = () {
166169
// We're asked to schedule a frame and call `frameHandler` when the frame

lib/web_ui/lib/src/engine/text/measurement.dart

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,6 @@ abstract class TextMeasurementService {
187187
static TextMeasurementService get canvasInstance =>
188188
CanvasTextMeasurementService.instance;
189189

190-
/// Whether the new experimental implementation of canvas-based text
191-
/// measurement is enabled or not.
192-
///
193-
/// This is only used for testing at the moment. Once the implementation is
194-
/// complete and production-ready, we'll get rid of this flag.
195-
static bool enableExperimentalCanvasImplementation = const bool.fromEnvironment('FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT', defaultValue: false);
196-
197190
/// Gets the appropriate [TextMeasurementService] instance for the given
198191
/// [paragraph].
199192
static TextMeasurementService forParagraph(ui.Paragraph paragraph) {
@@ -206,7 +199,7 @@ abstract class TextMeasurementService {
206199
// Skip using canvas measurements until the iframe becomes visible.
207200
// see: https://github.com/flutter/flutter/issues/36341
208201
if (!window.physicalSize.isEmpty &&
209-
enableExperimentalCanvasImplementation &&
202+
WebExperiments.instance.useCanvasText &&
210203
_canUseCanvasMeasurement(paragraph)) {
211204
return canvasInstance;
212205
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.6
6+
part of engine;
7+
8+
/// A bag of all experiment flags in the web engine.
9+
///
10+
/// This class also handles platform messages that can be sent to enable/disable
11+
/// certain experiments at runtime without the need to access engine internals.
12+
class WebExperiments {
13+
WebExperiments._() {
14+
js.context['_flutter_internal_update_experiment'] = updateExperiment;
15+
registerHotRestartListener(() {
16+
js.context['_flutter_internal_update_experiment'] = null;
17+
});
18+
}
19+
20+
static WebExperiments ensureInitialized() {
21+
if (WebExperiments.instance == null) {
22+
WebExperiments.instance = WebExperiments._();
23+
}
24+
return WebExperiments.instance;
25+
}
26+
27+
static WebExperiments instance;
28+
29+
/// Experiment flag for using canvas-based text measurement.
30+
bool get useCanvasText => _useCanvasText ?? false;
31+
set useCanvasText(bool enabled) {
32+
_useCanvasText = enabled;
33+
}
34+
35+
bool _useCanvasText = const bool.fromEnvironment(
36+
'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT',
37+
defaultValue: null,
38+
);
39+
40+
/// Reset all experimental flags to their default values.
41+
void reset() {
42+
_useCanvasText = null;
43+
}
44+
45+
/// Used to enable/disable experimental flags in the web engine.
46+
void updateExperiment(String name, bool enabled) {
47+
switch (name) {
48+
case 'useCanvasText':
49+
_useCanvasText = enabled;
50+
break;
51+
}
52+
}
53+
}

lib/web_ui/test/canvas_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import 'package:test/test.dart';
1111
import 'mock_engine_canvas.dart';
1212

1313
void main() {
14+
setUpAll(() {
15+
WebExperiments.ensureInitialized();
16+
});
17+
1418
group('EngineCanvas', () {
1519
MockEngineCanvas mockCanvas;
1620
ui.Paragraph paragraph;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.6
6+
import 'dart:html' as html;
7+
import 'dart:js_util' as js_util;
8+
9+
import 'package:test/test.dart';
10+
import 'package:ui/src/engine.dart';
11+
12+
void main() {
13+
setUp(() {
14+
WebExperiments.ensureInitialized();
15+
});
16+
17+
tearDown(() {
18+
WebExperiments.instance.reset();
19+
});
20+
21+
test('default web experiment values', () {
22+
expect(WebExperiments.instance.useCanvasText, false);
23+
});
24+
25+
test('can turn on/off web experiments', () {
26+
WebExperiments.instance.updateExperiment('useCanvasText', true);
27+
expect(WebExperiments.instance.useCanvasText, true);
28+
29+
WebExperiments.instance.updateExperiment('useCanvasText', false);
30+
expect(WebExperiments.instance.useCanvasText, false);
31+
32+
WebExperiments.instance.updateExperiment('useCanvasText', null);
33+
// Goes back to default value.
34+
expect(WebExperiments.instance.useCanvasText, false);
35+
});
36+
37+
test('ignores unknown experiments', () {
38+
expect(WebExperiments.instance.useCanvasText, false);
39+
WebExperiments.instance.updateExperiment('foobarbazqux', true);
40+
expect(WebExperiments.instance.useCanvasText, false);
41+
WebExperiments.instance.updateExperiment('foobarbazqux', false);
42+
expect(WebExperiments.instance.useCanvasText, false);
43+
});
44+
45+
test('can reset web experiments', () {
46+
WebExperiments.instance.updateExperiment('useCanvasText', true);
47+
WebExperiments.instance.reset();
48+
expect(WebExperiments.instance.useCanvasText, false);
49+
50+
WebExperiments.instance.updateExperiment('useCanvasText', true);
51+
WebExperiments.instance.updateExperiment('foobarbazqux', true);
52+
WebExperiments.instance.reset();
53+
expect(WebExperiments.instance.useCanvasText, false);
54+
});
55+
56+
test('js interop also works', () {
57+
expect(WebExperiments.instance.useCanvasText, false);
58+
59+
expect(() => jsUpdateExperiment('useCanvasText', true), returnsNormally);
60+
expect(WebExperiments.instance.useCanvasText, true);
61+
62+
expect(() => jsUpdateExperiment('useCanvasText', null), returnsNormally);
63+
expect(WebExperiments.instance.useCanvasText, false);
64+
});
65+
66+
test('js interop throws on wrong type', () {
67+
expect(() => jsUpdateExperiment(123, true), throwsA(anything));
68+
expect(() => jsUpdateExperiment('foo', 123), throwsA(anything));
69+
expect(() => jsUpdateExperiment('foo', 'bar'), throwsA(anything));
70+
expect(() => jsUpdateExperiment(false, 'foo'), throwsA(anything));
71+
});
72+
}
73+
74+
void jsUpdateExperiment(dynamic name, dynamic enabled) {
75+
js_util.callMethod(
76+
html.window,
77+
'_flutter_internal_update_experiment',
78+
<dynamic>[name, enabled],
79+
);
80+
}

lib/web_ui/test/golden_tests/engine/scuba.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class EngineScubaTester {
7171
sceneElement.append(canvas.rootElement);
7272
html.document.body.append(sceneElement);
7373
String screenshotName = '${fileName}_${canvas.runtimeType}';
74-
if (TextMeasurementService.enableExperimentalCanvasImplementation) {
74+
if (WebExperiments.instance.useCanvasText) {
7575
screenshotName += '+canvas_measurement';
7676
}
7777
await diffScreenshot(
@@ -96,18 +96,20 @@ void testEachCanvas(String description, CanvasTest body,
9696
test('$description (bitmap)', () {
9797
try {
9898
TextMeasurementService.initialize(rulerCacheCapacity: 2);
99+
WebExperiments.instance.useCanvasText = false;
99100
return body(BitmapCanvas(bounds));
100101
} finally {
102+
WebExperiments.instance.useCanvasText = null;
101103
TextMeasurementService.clearCache();
102104
}
103105
});
104106
test('$description (bitmap + canvas measurement)', () async {
105107
try {
106108
TextMeasurementService.initialize(rulerCacheCapacity: 2);
107-
TextMeasurementService.enableExperimentalCanvasImplementation = true;
109+
WebExperiments.instance.useCanvasText = true;
108110
await body(BitmapCanvas(bounds));
109111
} finally {
110-
TextMeasurementService.enableExperimentalCanvasImplementation = false;
112+
WebExperiments.instance.useCanvasText = null;
111113
TextMeasurementService.clearCache();
112114
}
113115
});

lib/web_ui/test/golden_tests/engine/text_overflow_golden_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// @dart = 2.6
66
import 'dart:async';
77

8-
import 'package:ui/ui.dart';
8+
import 'package:ui/ui.dart' hide window;
99
import 'package:ui/src/engine.dart';
1010

1111
import 'scuba.dart';
@@ -89,7 +89,7 @@ void main() async {
8989
offset = offset.translate(0, p.height + 10);
9090

9191
// Only the first line is rendered with an ellipsis.
92-
if (!TextMeasurementService.enableExperimentalCanvasImplementation) {
92+
if (!WebExperiments.instance.useCanvasText) {
9393
// This is now correct with the canvas-based measurement, so we shouldn't
9494
// print the "(wrong)" warning.
9595
p = warning('(wrong)');
@@ -106,7 +106,7 @@ void main() async {
106106

107107
// Only the first two lines are rendered and the ellipsis appears on the 2nd
108108
// line.
109-
if (!TextMeasurementService.enableExperimentalCanvasImplementation) {
109+
if (!WebExperiments.instance.useCanvasText) {
110110
// This is now correct with the canvas-based measurement, so we shouldn't
111111
// print the "(wrong)" warning.
112112
p = warning('(wrong)');

lib/web_ui/test/paragraph_builder_test.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
// found in the LICENSE file.
44

55
// @dart = 2.6
6+
import 'package:ui/src/engine.dart';
67
import 'package:ui/ui.dart';
78

89
import 'package:test/test.dart';
910

1011
void main() {
12+
setUpAll(() {
13+
WebExperiments.ensureInitialized();
14+
});
15+
1116
test('Should be able to build and layout a paragraph', () {
1217
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
1318
builder.addText('Hello');

lib/web_ui/test/paragraph_test.dart

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@
44

55
// @dart = 2.6
66
import 'package:ui/src/engine.dart';
7-
import 'package:ui/ui.dart';
7+
import 'package:ui/ui.dart' hide window;
88

99
import 'package:test/test.dart';
1010

1111
void testEachMeasurement(String description, VoidCallback body, {bool skip}) {
1212
test('$description (dom measurement)', () async {
1313
try {
1414
TextMeasurementService.initialize(rulerCacheCapacity: 2);
15+
WebExperiments.instance.useCanvasText = false;
1516
return body();
1617
} finally {
18+
WebExperiments.instance.useCanvasText = null;
1719
TextMeasurementService.clearCache();
1820
}
1921
}, skip: skip);
2022
test('$description (canvas measurement)', () async {
2123
try {
2224
TextMeasurementService.initialize(rulerCacheCapacity: 2);
23-
TextMeasurementService.enableExperimentalCanvasImplementation = true;
25+
WebExperiments.instance.useCanvasText = true;
2426
return body();
2527
} finally {
26-
TextMeasurementService.enableExperimentalCanvasImplementation = false;
28+
WebExperiments.instance.useCanvasText = null;
2729
TextMeasurementService.clearCache();
2830
}
2931
}, skip: skip);
@@ -184,7 +186,7 @@ void main() async {
184186
test('getPositionForOffset multi-line', () {
185187
// [Paragraph.getPositionForOffset] for multi-line text doesn't work well
186188
// with dom-based measurement.
187-
TextMeasurementService.enableExperimentalCanvasImplementation = true;
189+
WebExperiments.instance.useCanvasText = true;
188190
TextMeasurementService.initialize(rulerCacheCapacity: 2);
189191

190192
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
@@ -280,11 +282,11 @@ void main() async {
280282
);
281283

282284
TextMeasurementService.clearCache();
283-
TextMeasurementService.enableExperimentalCanvasImplementation = false;
285+
WebExperiments.instance.useCanvasText = null;
284286
});
285287

286288
test('getPositionForOffset multi-line centered', () {
287-
TextMeasurementService.enableExperimentalCanvasImplementation = true;
289+
WebExperiments.instance.useCanvasText = true;
288290
TextMeasurementService.initialize(rulerCacheCapacity: 2);
289291

290292
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
@@ -387,7 +389,7 @@ void main() async {
387389
);
388390

389391
TextMeasurementService.clearCache();
390-
TextMeasurementService.enableExperimentalCanvasImplementation = false;
392+
WebExperiments.instance.useCanvasText = null;
391393
});
392394

393395
testEachMeasurement('getBoxesForRange returns a box', () {
@@ -782,7 +784,7 @@ void main() async {
782784

783785
test('longestLine', () {
784786
// [Paragraph.longestLine] is only supported by canvas-based measurement.
785-
TextMeasurementService.enableExperimentalCanvasImplementation = true;
787+
WebExperiments.instance.useCanvasText = true;
786788
TextMeasurementService.initialize(rulerCacheCapacity: 2);
787789

788790
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
@@ -797,7 +799,7 @@ void main() async {
797799
expect(paragraph.longestLine, 50.0);
798800

799801
TextMeasurementService.clearCache();
800-
TextMeasurementService.enableExperimentalCanvasImplementation = false;
802+
WebExperiments.instance.useCanvasText = null;
801803
});
802804

803805
testEachMeasurement('getLineBoundary (single-line)', () {
@@ -824,7 +826,7 @@ void main() async {
824826
test('getLineBoundary (multi-line)', () {
825827
// [Paragraph.getLineBoundary] for multi-line paragraphs is only supported
826828
// by canvas-based measurement.
827-
TextMeasurementService.enableExperimentalCanvasImplementation = true;
829+
WebExperiments.instance.useCanvasText = true;
828830
TextMeasurementService.initialize(rulerCacheCapacity: 2);
829831

830832
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
@@ -867,7 +869,7 @@ void main() async {
867869
}
868870

869871
TextMeasurementService.clearCache();
870-
TextMeasurementService.enableExperimentalCanvasImplementation = false;
872+
WebExperiments.instance.useCanvasText = null;
871873
});
872874

873875
testEachMeasurement('width should be a whole integer', () {

0 commit comments

Comments
 (0)