mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Fix canvas2d leaks in text measurement (flutter/engine#38640)
* [web] Fix canvas2d leaks in text measurement * add a test * set context.font correctly * change the test to workaround weird safari behavior
This commit is contained in:
parent
2366b2c174
commit
383ce52b0d
@ -8,6 +8,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:js/js_util.dart' as js_util;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// This file contains static interop classes for interacting with the DOM and
|
||||
/// some helpers. All of the classes in this file are named after their
|
||||
@ -584,7 +585,16 @@ class DomPerformanceMeasure extends DomPerformanceEntry {}
|
||||
@staticInterop
|
||||
class DomCanvasElement extends DomHTMLElement {}
|
||||
|
||||
@visibleForTesting
|
||||
int debugCanvasCount = 0;
|
||||
|
||||
@visibleForTesting
|
||||
void debugResetCanvasCount() {
|
||||
debugCanvasCount = 0;
|
||||
}
|
||||
|
||||
DomCanvasElement createDomCanvasElement({int? width, int? height}) {
|
||||
debugCanvasCount++;
|
||||
final DomCanvasElement canvas =
|
||||
domWindow.document.createElement('canvas') as DomCanvasElement;
|
||||
if (width != null) {
|
||||
@ -625,6 +635,7 @@ abstract class DomCanvasImageSource {}
|
||||
class DomCanvasRenderingContext2D {}
|
||||
|
||||
extension DomCanvasRenderingContext2DExtension on DomCanvasRenderingContext2D {
|
||||
external DomCanvasElement? get canvas;
|
||||
external Object? get fillStyle;
|
||||
external set fillStyle(Object? style);
|
||||
external String get font;
|
||||
|
||||
@ -16,6 +16,16 @@ import 'paragraph.dart';
|
||||
import 'ruler.dart';
|
||||
import 'text_direction.dart';
|
||||
|
||||
/// A single canvas2d context to use for all text measurements.
|
||||
@visibleForTesting
|
||||
final DomCanvasRenderingContext2D textContext =
|
||||
// We don't use this canvas to draw anything, so let's make it as small as
|
||||
// possible to save memory.
|
||||
createDomCanvasElement(width: 0, height: 0).context2D;
|
||||
|
||||
/// The last font used in the [textContext].
|
||||
String? _lastContextFont;
|
||||
|
||||
/// Performs layout on a [CanvasParagraph].
|
||||
///
|
||||
/// It uses a [DomCanvasElement] to measure text.
|
||||
@ -24,9 +34,6 @@ class TextLayoutService {
|
||||
|
||||
final CanvasParagraph paragraph;
|
||||
|
||||
final DomCanvasRenderingContext2D context =
|
||||
createDomCanvasElement().context2D;
|
||||
|
||||
// *** Results of layout *** //
|
||||
|
||||
// Look at the Paragraph class for documentation of the following properties.
|
||||
@ -53,7 +60,7 @@ class TextLayoutService {
|
||||
ui.Rect get paintBounds => _paintBounds;
|
||||
ui.Rect _paintBounds = ui.Rect.zero;
|
||||
|
||||
late final Spanometer spanometer = Spanometer(paragraph, context);
|
||||
late final Spanometer spanometer = Spanometer(paragraph);
|
||||
|
||||
late final LayoutFragmenter layoutFragmenter =
|
||||
LayoutFragmenter(paragraph.plainText, paragraph.spans);
|
||||
@ -882,10 +889,9 @@ class LineBuilder {
|
||||
/// it's set, the [Spanometer] updates the underlying [context] so that
|
||||
/// subsequent measurements use the correct styles.
|
||||
class Spanometer {
|
||||
Spanometer(this.paragraph, this.context);
|
||||
Spanometer(this.paragraph);
|
||||
|
||||
final CanvasParagraph paragraph;
|
||||
final DomCanvasRenderingContext2D context;
|
||||
|
||||
static final RulerHost _rulerHost = RulerHost();
|
||||
|
||||
@ -904,8 +910,6 @@ class Spanometer {
|
||||
_rulers.clear();
|
||||
}
|
||||
|
||||
String _cssFontString = '';
|
||||
|
||||
double? get letterSpacing => currentSpan.style.letterSpacing;
|
||||
|
||||
TextHeightRuler? _currentRuler;
|
||||
@ -913,12 +917,24 @@ class Spanometer {
|
||||
|
||||
ParagraphSpan get currentSpan => _currentSpan!;
|
||||
set currentSpan(ParagraphSpan? span) {
|
||||
// Update the font string if it's different from the last applied font
|
||||
// string.
|
||||
//
|
||||
// Also, we need to update the font string even if the span isn't changing.
|
||||
// That's because `textContext` is shared across all spanometers.
|
||||
if (span != null) {
|
||||
final String newCssFontString = span.style.cssFontString;
|
||||
if (_lastContextFont != newCssFontString) {
|
||||
_lastContextFont = newCssFontString;
|
||||
textContext.font = newCssFontString;
|
||||
}
|
||||
}
|
||||
|
||||
if (span == _currentSpan) {
|
||||
return;
|
||||
}
|
||||
_currentSpan = span;
|
||||
|
||||
// No need to update css font string when `span` is null.
|
||||
if (span == null) {
|
||||
_currentRuler = null;
|
||||
return;
|
||||
@ -933,13 +949,6 @@ class Spanometer {
|
||||
_rulers[heightStyle] = ruler;
|
||||
}
|
||||
_currentRuler = ruler;
|
||||
|
||||
// Update the font string if it's different from the previous span.
|
||||
final String cssFontString = span.style.cssFontString;
|
||||
if (_cssFontString != cssFontString) {
|
||||
_cssFontString = cssFontString;
|
||||
context.font = cssFontString;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the spanometer is ready to take measurements.
|
||||
@ -955,7 +964,7 @@ class Spanometer {
|
||||
double get height => _currentRuler!.height;
|
||||
|
||||
double measureText(String text) {
|
||||
return measureSubstring(context, text, 0, text.length);
|
||||
return measureSubstring(textContext, text, 0, text.length);
|
||||
}
|
||||
|
||||
double measureRange(int start, int end) {
|
||||
@ -1047,7 +1056,7 @@ class Spanometer {
|
||||
assert(end >= currentSpan.start && end <= currentSpan.end);
|
||||
|
||||
return measureSubstring(
|
||||
context,
|
||||
textContext,
|
||||
paragraph.plainText,
|
||||
start,
|
||||
end,
|
||||
|
||||
@ -691,4 +691,64 @@ Future<void> testMain() async {
|
||||
l('i', 9, 10, hardBreak: true, width: 10.0, left: 40.0),
|
||||
]);
|
||||
});
|
||||
|
||||
test('uses a single minimal canvas', () {
|
||||
debugResetCanvasCount();
|
||||
|
||||
plain(ahemStyle, 'Lorem').layout(constrain(double.infinity));
|
||||
plain(ahemStyle, 'ipsum dolor').layout(constrain(150.0));
|
||||
// Try different styles too.
|
||||
plain(EngineParagraphStyle(fontWeight: ui.FontWeight.bold), 'sit amet').layout(constrain(300.0));
|
||||
|
||||
expect(textContext.canvas!.width, isZero);
|
||||
expect(textContext.canvas!.height, isZero);
|
||||
// This number is 0 instead of 1 because the canvas is created at the top
|
||||
// level as a global variable. So by the time this test runs, the canvas
|
||||
// would have been created already.
|
||||
//
|
||||
// So we just make sure that no new canvas is created after the above layout
|
||||
// calls.
|
||||
expect(debugCanvasCount, 0);
|
||||
});
|
||||
|
||||
test('does not leak styles across spanometers', () {
|
||||
// This prevents the Ahem font from being forced in all paragraphs.
|
||||
ui.debugEmulateFlutterTesterEnvironment = false;
|
||||
|
||||
final CanvasParagraph p1 = plain(
|
||||
EngineParagraphStyle(
|
||||
fontSize: 20.0,
|
||||
fontFamily: 'FontFamily1',
|
||||
),
|
||||
'Lorem',
|
||||
)..layout(constrain(double.infinity));
|
||||
// After the layout, the canvas should have the above style applied.
|
||||
expect(textContext.font, contains('20px'));
|
||||
expect(textContext.font, contains('FontFamily1'));
|
||||
|
||||
final CanvasParagraph p2 = plain(
|
||||
EngineParagraphStyle(
|
||||
fontSize: 40.0,
|
||||
fontFamily: 'FontFamily2',
|
||||
),
|
||||
'ipsum dolor',
|
||||
)..layout(constrain(double.infinity));
|
||||
// After the layout, the canvas should have the above style applied.
|
||||
expect(textContext.font, contains('40px'));
|
||||
expect(textContext.font, contains('FontFamily2'));
|
||||
|
||||
p1.getBoxesForRange(0, 2);
|
||||
// getBoxesForRange performs some text measurements. Let's make sure that it
|
||||
// applied the correct style.
|
||||
expect(textContext.font, contains('20px'));
|
||||
expect(textContext.font, contains('FontFamily1'));
|
||||
|
||||
p2.getBoxesForRange(0, 4);
|
||||
// getBoxesForRange performs some text measurements. Let's make sure that it
|
||||
// applied the correct style.
|
||||
expect(textContext.font, contains('40px'));
|
||||
expect(textContext.font, contains('FontFamily2'));
|
||||
|
||||
ui.debugEmulateFlutterTesterEnvironment = true;
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user