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
27 changes: 15 additions & 12 deletions lib/web_ui/lib/src/engine/canvas_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,10 @@ class _CanvasPool extends _SaveStackTracking {

void _createCanvas() {
bool requiresClearRect = false;
bool reused = false;
if (_reusablePool != null && _reusablePool.isNotEmpty) {
_canvas = _reusablePool.removeAt(0);
// If a canvas is the first element we set z-index = -1 to workaround
// blink compositing bug. To make sure this does not leak when reused
// reset z-index.
_canvas.style.removeProperty('z-index');
reused = true;
requiresClearRect = true;
} else {
// Compute the final CSS canvas size given the actual pixel count we
Expand Down Expand Up @@ -116,6 +114,12 @@ class _CanvasPool extends _SaveStackTracking {
..height = '${cssHeight}px';
}

// Before appending canvas, check if canvas is already on rootElement. This
// optimization prevents DOM .append call when a PersistentSurface is
// reused. Reading lastChild is faster than append call.
if (_rootElement.lastChild != _canvas) {
_rootElement.append(_canvas);
}
// When the picture has a 90-degree transform and clip in its
// ancestor layers, it triggers a bug in Blink and Webkit browsers
// that results in canvas obscuring text that should be painted on
Expand All @@ -126,16 +130,15 @@ class _CanvasPool extends _SaveStackTracking {
// Possible Blink bugs that are causing this:
// * https://bugs.chromium.org/p/chromium/issues/detail?id=370604
// * https://bugs.chromium.org/p/chromium/issues/detail?id=586601
final bool isFirstChildElement = _rootElement.firstChild == null;
if (isFirstChildElement) {
if (_rootElement.firstChild == _canvas) {
_canvas.style.zIndex = '-1';
} else if (reused) {
// If a canvas is the first element we set z-index = -1 to workaround
// blink compositing bug. To make sure this does not leak when reused
// reset z-index.
_canvas.style.removeProperty('z-index');
}
// Before appending canvas, check if canvas is already on rootElement. This
// optimization prevents DOM .append call when a PersistentSurface is
// reused. Reading lastChild is faster than append call.
if (_rootElement.lastChild != _canvas) {
_rootElement.append(_canvas);
}

_context = _canvas.context2D;
_contextHandle = ContextStateHandle(_context);
_initializeViewport(requiresClearRect);
Expand Down
62 changes: 62 additions & 0 deletions lib/web_ui/test/compositing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,39 @@ void main() {
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
skip: (browserEngine == BrowserEngine.firefox));
});

group('Compositing order', () {
// Regression test for https://github.com/flutter/flutter/issues/55058
//
// When BitmapCanvas uses multiple elements to paint, the very first
// canvas needs to have a -1 zIndex so it can preserve compositing order.
test('First canvas element should retain -1 zIndex after update', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture picture1 = _drawPicture();
EngineLayer oldLayer = builder.pushClipRect(
const Rect.fromLTRB(10, 10, 300, 300),
);
builder.addPicture(Offset.zero, picture1);
builder.pop();

html.HtmlElement content = builder.build().webOnlyRootElement;
expect(content.querySelector('canvas').style.zIndex, '-1');

// Force update to scene which will utilize reuse code path.
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
builder2.pushClipRect(
const Rect.fromLTRB(5, 10, 300, 300),
oldLayer: oldLayer
);
final Picture picture2 = _drawPicture();
builder2.addPicture(Offset.zero, picture2);
builder2.pop();

html.HtmlElement contentAfterReuse = builder2.build().webOnlyRootElement;
expect(contentAfterReuse.querySelector('canvas').style.zIndex, '-1');
});
});

}

typedef TestLayerBuilder = EngineLayer Function(
Expand Down Expand Up @@ -312,3 +345,32 @@ class MockPersistedPicture extends PersistedPicture {
@override
int get bitmapPixelCount => 0;
}

Picture _drawPicture() {
const double offsetX = 50;
const double offsetY = 50;
final EnginePictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas =
recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 10),
10,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(255, 0, 0, 1));
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 60),
10,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 255, 0, 1));
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 60),
10,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 0, 255, 1));
return recorder.endRecording();
}