diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart index 15c010c34cd..b98f969b1fb 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart @@ -156,55 +156,115 @@ ui.Rect computePlatformViewBounds(EmbeddedViewParams params) { /// [platformViews]. /// /// [paramsForViews] is required to compute the bounds of the platform views. +// TODO(harryterkelsen): Extend this to work for any sequence of platform views +// and pictures, https://github.com/flutter/flutter/issues/149863. Rendering createOptimizedRendering( List pictures, List platformViews, Map paramsForViews, ) { + final Map cachedComputedRects = {}; assert(pictures.length == platformViews.length + 1); final Rendering result = Rendering(); - // The first render canvas is required due to the pseudo-platform view "V_0" - // which is defined as a platform view that comes before all Flutter drawing - // commands and intersects with everything. - RenderingRenderCanvas currentRenderCanvas = RenderingRenderCanvas(); - - // This line essentially unwinds the first iteration of the following loop. - // Since "V_0" intersects with all subsequent pictures, then the first picture - // it intersects with is "P_0", so we create a new render canvas and add "P_0" - // to it. + // The first picture is added to the rendering in a new render canvas. + RenderingRenderCanvas tentativeCanvas = RenderingRenderCanvas(); if (!pictures[0].cullRect.isEmpty) { - currentRenderCanvas.add(pictures[0]); + tentativeCanvas.add(pictures[0]); } + for (int i = 0; i < platformViews.length; i++) { final RenderingPlatformView platformView = RenderingPlatformView(platformViews[i]); if (PlatformViewManager.instance.isVisible(platformViews[i])) { - final ui.Rect platformViewBounds = + final ui.Rect platformViewBounds = cachedComputedRects[platformViews[i]] = computePlatformViewBounds(paramsForViews[platformViews[i]]!); + if (debugOverlayOptimizationBounds) { platformView.debugComputedBounds = platformViewBounds; } - bool intersectsWithCurrentPictures = false; - for (final CkPicture picture in currentRenderCanvas.pictures) { - if (picture.cullRect.overlaps(platformViewBounds)) { - intersectsWithCurrentPictures = true; + + // If the platform view intersects with any pictures in the tentative canvas + // then add the tentative canvas to the rendering. + for (final CkPicture picture in tentativeCanvas.pictures) { + if (!picture.cullRect.intersect(platformViewBounds).isEmpty) { + result.add(tentativeCanvas); + tentativeCanvas = RenderingRenderCanvas(); break; } } - if (intersectsWithCurrentPictures) { - result.add(currentRenderCanvas); - currentRenderCanvas = RenderingRenderCanvas(); - } } result.add(platformView); - if (!pictures[i + 1].cullRect.isEmpty) { - currentRenderCanvas.add(pictures[i + 1]); + + if (pictures[i + 1].cullRect.isEmpty) { + continue; + } + + // Find the first render canvas which comes after the last entity (picture + // or platform view) that the next picture intersects with, and add the + // picture to that render canvas, or create a new render canvas. + + // First check if the picture intersects with any pictures in the tentative + // canvas, as this will be the last canvas in the rendering when it is + // eventually added. + bool addedToTentativeCanvas = false; + for (final CkPicture picture in tentativeCanvas.pictures) { + if (!picture.cullRect.intersect(pictures[i + 1].cullRect).isEmpty) { + tentativeCanvas.add(pictures[i + 1]); + addedToTentativeCanvas = true; + break; + } + } + if (addedToTentativeCanvas) { + continue; + } + + RenderingRenderCanvas? lastCanvasSeen; + bool addedPictureToRendering = false; + for (final RenderingEntity entity in result.entities.reversed) { + if (entity is RenderingPlatformView) { + if (PlatformViewManager.instance.isVisible(entity.viewId)) { + final ui.Rect platformViewBounds = + cachedComputedRects[entity.viewId]!; + if (!platformViewBounds.intersect(pictures[i + 1].cullRect).isEmpty) { + // The next picture intersects with a platform view already in the + // result. Add this picture to the first render canvas which comes + // after this platform view or create one if none exists. + if (lastCanvasSeen != null) { + lastCanvasSeen.add(pictures[i + 1]); + } else { + tentativeCanvas.add(pictures[i + 1]); + } + addedPictureToRendering = true; + break; + } + } + } else if (entity is RenderingRenderCanvas) { + lastCanvasSeen = entity; + // Check if we intersect with any pictures in this render canvas. + for (final CkPicture picture in entity.pictures) { + if (!picture.cullRect.intersect(pictures[i + 1].cullRect).isEmpty) { + lastCanvasSeen.add(pictures[i + 1]); + addedPictureToRendering = true; + break; + } + } + } + } + if (!addedPictureToRendering) { + if (lastCanvasSeen != null) { + // Add it to the last canvas seen in the rendering, if any. + lastCanvasSeen.add(pictures[i + 1]); + } else { + tentativeCanvas.add(pictures[i + 1]); + } } } - if (currentRenderCanvas.pictures.isNotEmpty) { - result.add(currentRenderCanvas); + + if (tentativeCanvas.pictures.isNotEmpty) { + result.add(tentativeCanvas); } + return result; } diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart index 0301f2eb25f..99b540884a8 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -1120,9 +1120,135 @@ void testMain() { _platformView, _overlay, ]); + + // Scene 4: Same as scene 1 but with a placeholder rectangle painted + // under each platform view. This is closer to how the real Flutter + // framework would render a grid of platform views. Interestingly, in this + // case every drawing can go in a base canvas. + final LayerSceneBuilder sb4 = LayerSceneBuilder(); + sb4.pushOffset(0, 0); + sb4.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 300, 300))); + sb4.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(10, 10, 50, 50))); + sb4.addPlatformView(0, + offset: const ui.Offset(10, 10), width: 50, height: 50); + sb4.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(70, 10, 50, 50))); + sb4.addPlatformView(1, + offset: const ui.Offset(70, 10), width: 50, height: 50); + sb4.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(130, 10, 50, 50))); + sb4.addPlatformView(2, + offset: const ui.Offset(130, 10), width: 50, height: 50); + final LayerScene scene4 = sb4.build(); + await renderScene(scene4); + _expectSceneMatches(<_EmbeddedViewMarker>[ + _overlay, + _platformView, + _platformView, + _platformView, + ]); + + // Scene 5: A combination of scene 1 and scene 4, where a subtitle is + // painted over each platform view and a placeholder is painted under each + // one. Unfortunately, we need an overlay for each platform view in this + // case. + final LayerSceneBuilder sb5 = LayerSceneBuilder(); + sb5.pushOffset(0, 0); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 300, 300))); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(10, 10, 50, 50))); + sb5.addPlatformView(0, + offset: const ui.Offset(10, 10), width: 50, height: 50); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(12, 12, 10, 10))); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(70, 10, 50, 50))); + sb5.addPlatformView(1, + offset: const ui.Offset(70, 10), width: 50, height: 50); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(72, 12, 10, 10))); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(130, 10, 50, 50))); + sb5.addPlatformView(2, + offset: const ui.Offset(130, 10), width: 50, height: 50); + sb5.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(132, 12, 10, 10))); + final LayerScene scene5 = sb5.build(); + await renderScene(scene5); + _expectSceneMatches(<_EmbeddedViewMarker>[ + _overlay, + _platformView, + _overlay, + _platformView, + _overlay, + _platformView, + _overlay, + ]); }); - test('sinks platform view under the canvas if it does not overlap with the picture', + test( + 'correctly places pictures in case where next ' + 'picture intersects multiple elements', () async { + ui_web.platformViewRegistry.registerViewFactory( + 'test-view', + (int viewId) => createDomHTMLDivElement()..className = 'platform-view', + ); + ui_web.platformViewRegistry.registerViewFactory( + 'invisible-view', + (int viewId) => + createDomHTMLDivElement()..className = 'invisible-platform-view', + isVisible: false, + ); + + CkPicture rectPicture(ui.Rect rect) { + return paintPicture(rect, (CkCanvas canvas) { + canvas.drawRect( + rect, CkPaint()..color = const ui.Color.fromARGB(255, 255, 0, 0)); + }); + } + + await createPlatformView(0, 'test-view'); + await createPlatformView(1, 'invisible-view'); + + expect(PlatformViewManager.instance.isVisible(0), isTrue); + expect(PlatformViewManager.instance.isVisible(1), isFalse); + + final LayerSceneBuilder sb = LayerSceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 100, 100))); + sb.addPlatformView(0, + offset: const ui.Offset(10, 10), width: 50, height: 50); + sb.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 100, 100))); + sb.addPlatformView(1, + offset: const ui.Offset(10, 10), width: 50, height: 50); + sb.addPicture( + ui.Offset.zero, rectPicture(const ui.Rect.fromLTWH(0, 0, 5, 5))); + final LayerScene scene = sb.build(); + await renderScene(scene); + _expectSceneMatches(<_EmbeddedViewMarker>[ + _overlay, + _platformView, + _platformView, + _overlay, + ]); + + final Rendering rendering = CanvasKitRenderer.instance + .debugGetRasterizerForView(implicitView)! + .viewEmbedder + .debugActiveRendering; + final List picturesPerCanvas = rendering.canvases + .map((RenderingRenderCanvas canvas) => canvas.pictures.length) + .toList(); + expect(picturesPerCanvas, [1, 2]); + }); + + test( + 'sinks platform view under the canvas if it does not overlap with the picture', () async { ui_web.platformViewRegistry.registerViewFactory( 'test-view',