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
33 changes: 21 additions & 12 deletions lib/web_ui/lib/src/engine/canvas_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,6 @@ class CanvasPool extends _SaveStackTracking {
translate(transform.dx, transform.dy);
}

/// Returns true if no canvas has been allocated yet.
bool get isEmpty => _canvas == null;

/// Returns true if a canvas has been allocated for use.
bool get isNotEmpty => _canvas != null;


/// Returns [CanvasRenderingContext2D] api to draw into this canvas.
html.CanvasRenderingContext2D get context {
html.CanvasRenderingContext2D? ctx = _context;
Expand All @@ -106,12 +99,28 @@ class CanvasPool extends _SaveStackTracking {
return _contextHandle!;
}

/// Prevents active canvas to be used for rendering and prepares a new
/// canvas allocation on next drawing request that will require one.
/// Returns true if a canvas is currently available for drawing.
///
/// Calling [contextHandle] or, transitively, any of the `draw*` methods while
/// this returns true will reuse the existing canvas. Otherwise, a new canvas
/// will be allocated.
///
/// Previously allocated and closed canvases (see [closeCanvas]) are not
/// considered by this getter.
bool get hasCanvas => _canvas != null;

/// Stops the currently available canvas from receiving any further drawing
/// commands.
///
/// After calling this method, a subsequent call to [contextHandle] or,
/// transitively, any of the `draw*` methods will cause a new canvas to be
/// allocated.
///
/// Saves current canvas so we can dispose
/// and replay the clip/transform stack on top of new canvas.
void closeCurrentCanvas() {
/// The closed canvas becomes an "active" canvas, that is a canvas that's used
/// to render picture content in the current frame. Active canvases may be
/// reused in other pictures if their contents are no longer needed for this
/// picture.
void closeCanvas() {
assert(_rootElement != null);
// Place clean copy of current canvas with context stack restored and paint
// reset into pool.
Expand Down
44 changes: 26 additions & 18 deletions lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class BitmapCanvas extends EngineCanvas {
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
(_childOverdraw &&
_canvasPool.isEmpty &&
!_canvasPool.hasCanvas &&
paint.maskFilter == null &&
paint.shader == null &&
paint.style != ui.PaintingStyle.stroke);
Expand All @@ -384,7 +384,7 @@ class BitmapCanvas extends EngineCanvas {
((_childOverdraw ||
_renderStrategy.hasImageElements ||
_renderStrategy.hasParagraphs) &&
_canvasPool.isEmpty &&
!_canvasPool.hasCanvas &&
paint.maskFilter == null &&
paint.shader == null);

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

@override
Expand Down Expand Up @@ -626,7 +626,7 @@ class BitmapCanvas extends EngineCanvas {
_applyTargetSize(
imageElement, image.width.toDouble(), image.height.toDouble());
}
_closeCurrentCanvas();
_closeCanvas();
}

html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) {
Expand Down Expand Up @@ -770,7 +770,7 @@ class BitmapCanvas extends EngineCanvas {
restore();
}
}
_closeCurrentCanvas();
_closeCanvas();
}

void _applyTargetSize(
Expand Down Expand Up @@ -882,8 +882,8 @@ class BitmapCanvas extends EngineCanvas {
// |--- <img>
// Any drawing operations after these tags should allocate a new canvas,
// instead of drawing into earlier canvas.
void _closeCurrentCanvas() {
_canvasPool.closeCurrentCanvas();
void _closeCanvas() {
_canvasPool.closeCanvas();
_childOverdraw = true;
_cachedLastCssFont = null;
}
Expand Down Expand Up @@ -939,16 +939,24 @@ class BitmapCanvas extends EngineCanvas {
void drawParagraph(CanvasParagraph paragraph, ui.Offset offset) {
assert(paragraph.isLaidOut);

/// - paragraph.drawOnCanvas checks that the text styling doesn't include
/// features that prevent text from being rendered correctly using canvas.
/// - _childOverdraw check prevents sandwitching multiple canvas elements
/// when we have alternating paragraphs and other drawing commands that are
/// suitable for canvas.
/// - To make sure an svg filter is applied correctly to paragraph we
/// check isInsideSvgFilterTree to make sure dom node doesn't have any
/// parents that apply one.
if (paragraph.drawOnCanvas && _childOverdraw == false &&
!_renderStrategy.isInsideSvgFilterTree) {
// Normally, text is composited as a plain HTML <p> tag. However, if a
// bitmap canvas was used for a preceding drawing command, then it's more
// efficient to continue compositing into the existing canvas, if possible.
// Whether it's possible to composite a paragraph into a 2D canvas depends
// on the following:
final bool canCompositeIntoBitmapCanvas =
// Cannot composite if the paragraph cannot be drawn into bitmap canvas
// in the first place.
paragraph.canDrawOnCanvas &&
// Cannot composite if there's no bitmap canvas to composite into.
// Creating a new bitmap canvas just to draw text doesn't make sense.
_canvasPool.hasCanvas &&
!_childOverdraw &&
// Bitmap canvas introduces correctness issues in the presence of SVG
// filters, so prefer plain HTML in this case.
!_renderStrategy.isInsideSvgFilterTree;

if (canCompositeIntoBitmapCanvas) {
paragraph.paint(this, offset);
return;
}
Expand Down Expand Up @@ -977,7 +985,7 @@ class BitmapCanvas extends EngineCanvas {
paragraphElement.style
..left = '0px'
..top = '0px';
_closeCurrentCanvas();
_closeCanvas();
}

/// Draws vertices on a gl context.
Expand Down
23 changes: 13 additions & 10 deletions lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class CanvasParagraph implements ui.Paragraph {
required this.paragraphStyle,
required this.plainText,
required this.placeholderCount,
required this.drawOnCanvas,
required this.canDrawOnCanvas,
});

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

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

@override
double get width => _layoutService.width;
Expand Down Expand Up @@ -623,7 +626,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
}
}

bool _drawOnCanvas = true;
bool _canDrawOnCanvas = true;

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

if (_drawOnCanvas) {
if (_canDrawOnCanvas) {
final ui.TextDecoration? decoration = style.decoration;
if (decoration != null && decoration != ui.TextDecoration.none) {
_drawOnCanvas = false;
_canDrawOnCanvas = false;
}
}

if (_drawOnCanvas) {
if (_canDrawOnCanvas) {
final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
if (fontFeatures != null && fontFeatures.isNotEmpty) {
_drawOnCanvas = false;
_canDrawOnCanvas = false;
}
}

if (_drawOnCanvas) {
if (_canDrawOnCanvas) {
final List<ui.FontVariation>? fontVariations = style.fontVariations;
if (fontVariations != null && fontVariations.isNotEmpty) {
_drawOnCanvas = false;
_canDrawOnCanvas = false;
}
}

Expand All @@ -663,7 +666,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
paragraphStyle: _paragraphStyle,
plainText: _plainTextBuffer.toString(),
placeholderCount: _placeholderCount,
drawOnCanvas: _drawOnCanvas,
canDrawOnCanvas: _canDrawOnCanvas,
);
}
}
22 changes: 22 additions & 0 deletions lib/web_ui/test/html/bitmap_canvas_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,26 @@ Future<void> testMain() async {
pixelComparison: PixelComparison.precise,
);
});

// Regression test for https://github.com/flutter/flutter/issues/96498. When
// a picture is made of just text that can be rendered using plain HTML,
// BitmapCanvas should not create any <canvas> elements as they are expensive.
test('does not allocate bitmap canvas just for text', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy());

final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'));
builder.addText('Hello');
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
paragraph.layout(const ParagraphConstraints(width: 1000));

canvas.drawParagraph(paragraph, const Offset(8.5, 8.5));
expect(
canvas.rootElement.querySelectorAll('canvas'),
isEmpty,
);
expect(
canvas.rootElement.querySelectorAll('flt-paragraph').single.innerText,
'Hello',
);
});
}
4 changes: 2 additions & 2 deletions lib/web_ui/test/html/compositing/compositing_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ void _testCullRectComputation() {
final RecordingCanvas canvas = recorder.beginRecording(outerClip);
canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5));
final ui.Picture picture = recorder.endRecording();
expect(paragraph.drawOnCanvas, isFalse);
expect(paragraph.canDrawOnCanvas, isFalse);

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

builder.addPicture(
ui.Offset.zero,
Expand Down