Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 661e209

Browse files
committed
[web] do not allocate canvases just for text
1 parent 14d7287 commit 661e209

File tree

5 files changed

+84
-42
lines changed

5 files changed

+84
-42
lines changed

lib/web_ui/lib/src/engine/canvas_pool.dart

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,6 @@ class CanvasPool extends _SaveStackTracking {
7676
translate(transform.dx, transform.dy);
7777
}
7878

79-
/// Returns true if no canvas has been allocated yet.
80-
bool get isEmpty => _canvas == null;
81-
82-
/// Returns true if a canvas has been allocated for use.
83-
bool get isNotEmpty => _canvas != null;
84-
85-
8679
/// Returns [CanvasRenderingContext2D] api to draw into this canvas.
8780
html.CanvasRenderingContext2D get context {
8881
html.CanvasRenderingContext2D? ctx = _context;
@@ -106,12 +99,28 @@ class CanvasPool extends _SaveStackTracking {
10699
return _contextHandle!;
107100
}
108101

109-
/// Prevents active canvas to be used for rendering and prepares a new
110-
/// canvas allocation on next drawing request that will require one.
102+
/// Returns true if a canvas is currently available for drawing.
103+
///
104+
/// Calling [contextHandle] or, transitively, any of the `draw*` methods while
105+
/// this returns true will reuse the existing canvas. Otherwise, a new canvas
106+
/// will be allocated.
107+
///
108+
/// Previously allocated and closed canvases (see [closeCanvas]) are not
109+
/// considered by this getter.
110+
bool get hasCanvas => _canvas != null;
111+
112+
/// Stops the currently available canvas from receiving any further drawing
113+
/// commands.
114+
///
115+
/// After calling this method, a subsequent call to [contextHandle] or,
116+
/// transitively, any of the `draw*` methods will cause a new canvas to be
117+
/// allocated.
111118
///
112-
/// Saves current canvas so we can dispose
113-
/// and replay the clip/transform stack on top of new canvas.
114-
void closeCurrentCanvas() {
119+
/// The closed canvas becomes an "active" canvas, that is a canvas that's used
120+
/// to render picture content in the current frame. Active canvases may be
121+
/// reused in other pictures if their contents are no longer needed for this
122+
/// picture.
123+
void closeCanvas() {
115124
assert(_rootElement != null);
116125
// Place clean copy of current canvas with context stack restored and paint
117126
// reset into pool.

lib/web_ui/lib/src/engine/html/bitmap_canvas.dart

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ class BitmapCanvas extends EngineCanvas {
370370
_renderStrategy.isInsideSvgFilterTree ||
371371
(_preserveImageData == false && _contains3dTransform) ||
372372
(_childOverdraw &&
373-
_canvasPool.isEmpty &&
373+
!_canvasPool.hasCanvas &&
374374
paint.maskFilter == null &&
375375
paint.shader == null &&
376376
paint.style != ui.PaintingStyle.stroke);
@@ -384,7 +384,7 @@ class BitmapCanvas extends EngineCanvas {
384384
((_childOverdraw ||
385385
_renderStrategy.hasImageElements ||
386386
_renderStrategy.hasParagraphs) &&
387-
_canvasPool.isEmpty &&
387+
!_canvasPool.hasCanvas &&
388388
paint.maskFilter == null &&
389389
paint.shader == null);
390390

@@ -469,7 +469,7 @@ class BitmapCanvas extends EngineCanvas {
469469
element.style.mixBlendMode = blendModeToCssMixBlendMode(blendMode) ?? '';
470470
}
471471
// Switch to preferring DOM from now on, and close the current canvas.
472-
_closeCurrentCanvas();
472+
_closeCanvas();
473473
}
474474

475475
@override
@@ -626,7 +626,7 @@ class BitmapCanvas extends EngineCanvas {
626626
_applyTargetSize(
627627
imageElement, image.width.toDouble(), image.height.toDouble());
628628
}
629-
_closeCurrentCanvas();
629+
_closeCanvas();
630630
}
631631

632632
html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) {
@@ -770,7 +770,7 @@ class BitmapCanvas extends EngineCanvas {
770770
restore();
771771
}
772772
}
773-
_closeCurrentCanvas();
773+
_closeCanvas();
774774
}
775775

776776
void _applyTargetSize(
@@ -882,8 +882,8 @@ class BitmapCanvas extends EngineCanvas {
882882
// |--- <img>
883883
// Any drawing operations after these tags should allocate a new canvas,
884884
// instead of drawing into earlier canvas.
885-
void _closeCurrentCanvas() {
886-
_canvasPool.closeCurrentCanvas();
885+
void _closeCanvas() {
886+
_canvasPool.closeCanvas();
887887
_childOverdraw = true;
888888
_cachedLastCssFont = null;
889889
}
@@ -939,16 +939,24 @@ class BitmapCanvas extends EngineCanvas {
939939
void drawParagraph(CanvasParagraph paragraph, ui.Offset offset) {
940940
assert(paragraph.isLaidOut);
941941

942-
/// - paragraph.drawOnCanvas checks that the text styling doesn't include
943-
/// features that prevent text from being rendered correctly using canvas.
944-
/// - _childOverdraw check prevents sandwitching multiple canvas elements
945-
/// when we have alternating paragraphs and other drawing commands that are
946-
/// suitable for canvas.
947-
/// - To make sure an svg filter is applied correctly to paragraph we
948-
/// check isInsideSvgFilterTree to make sure dom node doesn't have any
949-
/// parents that apply one.
950-
if (paragraph.drawOnCanvas && _childOverdraw == false &&
951-
!_renderStrategy.isInsideSvgFilterTree) {
942+
// Normally, text is composited as a plain HTML <p> tag. However, if a
943+
// bitmap canvas was used for a preceding drawing command, then it's more
944+
// efficient to continue compositing into the existing canvas, if possible.
945+
// Whether it's possible to composite a paragraph into a 2D canvas depends
946+
// on the following:
947+
final bool canCompositeIntoBitmapCanvas =
948+
// Cannot composite if the paragraph cannot be drawn into bitmap canvas
949+
// in the first place.
950+
paragraph.canDrawOnCanvas &&
951+
// Cannot composite if there's no bitmap canvas to composite into.
952+
// Creating a new bitmap canvas just to draw text doesn't make sense.
953+
_canvasPool.hasCanvas &&
954+
!_childOverdraw &&
955+
// Bitmap canvas introduces correctness issues in the presence of SVG
956+
// filters, so prefer plain HTML in this case.
957+
!_renderStrategy.isInsideSvgFilterTree;
958+
959+
if (canCompositeIntoBitmapCanvas) {
952960
paragraph.paint(this, offset);
953961
return;
954962
}
@@ -977,7 +985,7 @@ class BitmapCanvas extends EngineCanvas {
977985
paragraphElement.style
978986
..left = '0px'
979987
..top = '0px';
980-
_closeCurrentCanvas();
988+
_closeCanvas();
981989
}
982990

983991
/// Draws vertices on a gl context.

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CanvasParagraph implements ui.Paragraph {
3131
required this.paragraphStyle,
3232
required this.plainText,
3333
required this.placeholderCount,
34-
required this.drawOnCanvas,
34+
required this.canDrawOnCanvas,
3535
});
3636

3737
/// The flat list of spans that make up this paragraph.
@@ -47,7 +47,10 @@ class CanvasParagraph implements ui.Paragraph {
4747
final int placeholderCount;
4848

4949
/// Whether this paragraph can be drawn on a bitmap canvas.
50-
final bool drawOnCanvas;
50+
///
51+
/// Some text features cannot be rendered into a 2D canvas and must use HTML,
52+
/// such as font features and text decorations.
53+
final bool canDrawOnCanvas;
5154

5255
@override
5356
double get width => _layoutService.width;
@@ -623,7 +626,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
623626
}
624627
}
625628

626-
bool _drawOnCanvas = true;
629+
bool _canDrawOnCanvas = true;
627630

628631
@override
629632
void addText(String text) {
@@ -632,24 +635,24 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
632635
_plainTextBuffer.write(text);
633636
final int end = _plainTextBuffer.length;
634637

635-
if (_drawOnCanvas) {
638+
if (_canDrawOnCanvas) {
636639
final ui.TextDecoration? decoration = style.decoration;
637640
if (decoration != null && decoration != ui.TextDecoration.none) {
638-
_drawOnCanvas = false;
641+
_canDrawOnCanvas = false;
639642
}
640643
}
641644

642-
if (_drawOnCanvas) {
645+
if (_canDrawOnCanvas) {
643646
final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
644647
if (fontFeatures != null && fontFeatures.isNotEmpty) {
645-
_drawOnCanvas = false;
648+
_canDrawOnCanvas = false;
646649
}
647650
}
648651

649-
if (_drawOnCanvas) {
652+
if (_canDrawOnCanvas) {
650653
final List<ui.FontVariation>? fontVariations = style.fontVariations;
651654
if (fontVariations != null && fontVariations.isNotEmpty) {
652-
_drawOnCanvas = false;
655+
_canDrawOnCanvas = false;
653656
}
654657
}
655658

@@ -663,7 +666,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
663666
paragraphStyle: _paragraphStyle,
664667
plainText: _plainTextBuffer.toString(),
665668
placeholderCount: _placeholderCount,
666-
drawOnCanvas: _drawOnCanvas,
669+
canDrawOnCanvas: _canDrawOnCanvas,
667670
);
668671
}
669672
}

lib/web_ui/test/html/bitmap_canvas_golden_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,26 @@ Future<void> testMain() async {
270270
pixelComparison: PixelComparison.precise,
271271
);
272272
});
273+
274+
// Regression test for https://github.com/flutter/flutter/issues/96498. When
275+
// a picture is made of just text that can be rendered using plain HTML,
276+
// BitmapCanvas should not create any <canvas> elements as they are expensive.
277+
test('does not allocate bitmap canvas just for text', () async {
278+
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy());
279+
280+
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'));
281+
builder.addText('Hello');
282+
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
283+
paragraph.layout(const ParagraphConstraints(width: 1000));
284+
285+
canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
286+
expect(
287+
canvas.rootElement.querySelectorAll('canvas'),
288+
isEmpty,
289+
);
290+
expect(
291+
canvas.rootElement.querySelectorAll('flt-paragraph').single.innerText,
292+
'Hello',
293+
);
294+
});
273295
}

lib/web_ui/test/html/compositing/compositing_golden_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,7 @@ void _testCullRectComputation() {
847847
final RecordingCanvas canvas = recorder.beginRecording(outerClip);
848848
canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5));
849849
final ui.Picture picture = recorder.endRecording();
850-
expect(paragraph.drawOnCanvas, isFalse);
850+
expect(paragraph.canDrawOnCanvas, isFalse);
851851

852852
builder.addPicture(
853853
ui.Offset.zero,
@@ -861,7 +861,7 @@ void _testCullRectComputation() {
861861
final RecordingCanvas canvas = recorder.beginRecording(innerClip);
862862
canvas.drawParagraph(paragraph, ui.Offset(8.5, 8.5 + innerClip.top));
863863
final ui.Picture picture = recorder.endRecording();
864-
expect(paragraph.drawOnCanvas, isFalse);
864+
expect(paragraph.canDrawOnCanvas, isFalse);
865865

866866
builder.addPicture(
867867
ui.Offset.zero,

0 commit comments

Comments
 (0)