Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
9 changes: 1 addition & 8 deletions lib/web_ui/lib/src/engine/text/measurement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
53 changes: 53 additions & 0 deletions lib/web_ui/lib/src/engine/web_experiments.dart
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this works without jsify or allowInterop or some such 🤔

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does allowing null help with anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. In tests, we set the flag to a specific value (true or false), then we set it back to null when the test is done. The value null means "use the default value".

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;
}
}
}
4 changes: 4 additions & 0 deletions lib/web_ui/test/canvas_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions lib/web_ui/test/engine/web_experiments_test.dart
Original file line number Diff line number Diff line change
@@ -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',
<dynamic>[name, enabled],
);
}
8 changes: 5 additions & 3 deletions lib/web_ui/test/golden_tests/engine/scuba.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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();
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)');
Expand All @@ -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)');
Expand Down
5 changes: 5 additions & 0 deletions lib/web_ui/test/paragraph_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
24 changes: 13 additions & 11 deletions lib/web_ui/test/paragraph_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@

// @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';

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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -387,7 +389,7 @@ void main() async {
);

TextMeasurementService.clearCache();
TextMeasurementService.enableExperimentalCanvasImplementation = false;
WebExperiments.instance.useCanvasText = null;
});

testEachMeasurement('getBoxesForRange returns a box', () {
Expand Down Expand Up @@ -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(
Expand All @@ -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)', () {
Expand All @@ -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(
Expand Down Expand Up @@ -867,7 +869,7 @@ void main() async {
}

TextMeasurementService.clearCache();
TextMeasurementService.enableExperimentalCanvasImplementation = false;
WebExperiments.instance.useCanvasText = null;
});

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