diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index ff5c7006f2..d78e0b1754 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -401,7 +401,7 @@ class CanvasKitRenderer implements Renderer { // TODO(harryterkelsen): Merge this logic with the async logic in // [EngineScene], https://github.com/flutter/flutter/issues/142072. @override - Future renderScene(ui.Scene scene, ui.FlutterView view) async { + Future renderScene(ui.Scene scene, EngineFlutterView view) async { assert(_rasterizers.containsKey(view.viewId), "Unable to render to a view which hasn't been registered"); final ViewRasterizer rasterizer = _rasterizers[view.viewId]!; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart index 5a47ba40c5..1bcdc2f53a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart @@ -302,7 +302,7 @@ class HtmlRenderer implements Renderer { CanvasParagraphBuilder(style as EngineParagraphStyle); @override - Future renderScene(ui.Scene scene, ui.FlutterView view) async { + Future renderScene(ui.Scene scene, EngineFlutterView view) async { final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; scene as SurfaceScene; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart index 3337ebf3ea..df2c9eeff7 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart @@ -225,5 +225,5 @@ abstract class Renderer { ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style); - Future renderScene(ui.Scene scene, ui.FlutterView view); + Future renderScene(ui.Scene scene, EngineFlutterView view); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart index 61cb776a6d..53504204a4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart @@ -44,7 +44,7 @@ class _SceneRender { // This class builds a DOM tree that composites an `EngineScene`. class EngineSceneView { - factory EngineSceneView(PictureRenderer pictureRenderer, ui.FlutterView flutterView) { + factory EngineSceneView(PictureRenderer pictureRenderer, EngineFlutterView flutterView) { final DomElement sceneElement = createDomElement('flt-scene'); return EngineSceneView._(pictureRenderer, flutterView, sceneElement); } @@ -53,7 +53,7 @@ class EngineSceneView { final PictureRenderer pictureRenderer; final DomElement sceneElement; - final ui.FlutterView flutterView; + final EngineFlutterView flutterView; List containers = []; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index 889067fcae..c73c63d006 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -14,7 +14,7 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web; class SkwasmRenderer implements Renderer { late SkwasmSurface surface; - EngineSceneView? _sceneView; + final Map _sceneViews = {}; @override final SkwasmFontCollection fontCollection = SkwasmFontCollection(); @@ -397,27 +397,21 @@ class SkwasmRenderer implements Renderer { return decoder; } - // TODO(harryterkelsen): Add multiview support, - // https://github.com/flutter/flutter/issues/137073. @override - Future renderScene(ui.Scene scene, ui.FlutterView view) { + Future renderScene(ui.Scene scene, EngineFlutterView view) { final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; recorder?.recordBuildFinish(); - view as EngineFlutterView; - assert(view is EngineFlutterWindow, 'Skwasm does not support multi-view mode yet'); final EngineSceneView sceneView = _getSceneViewForView(view); return sceneView.renderScene(scene as EngineScene, recorder); } EngineSceneView _getSceneViewForView(EngineFlutterView view) { - // TODO(mdebbar): Support multi-view mode. - if (_sceneView == null) { - _sceneView = EngineSceneView(SkwasmPictureRenderer(surface), view); - final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; - implicitView.dom.setScene(_sceneView!.sceneElement); - } - return _sceneView!; + return _sceneViews.putIfAbsent(view, () { + final EngineSceneView sceneView = EngineSceneView(SkwasmPictureRenderer(surface), view); + view.dom.setScene(sceneView.sceneElement); + return sceneView; + }); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart index 79d888e785..f11d92166f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart @@ -44,7 +44,7 @@ int _nextViewId = kImplicitViewId + 1; /// /// In addition to everything defined in [ui.FlutterView], this class adds /// a few web-specific properties. -base class EngineFlutterView implements ui.FlutterView { +class EngineFlutterView implements ui.FlutterView { /// Creates a [ui.FlutterView] that can be used in multi-view mode. /// /// The [hostElement] parameter specifies the container in the DOM into which diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart index cc69142d8b..b362412368 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart @@ -33,7 +33,7 @@ void setUpCanvasKitTest({bool withImplicitView = false}) { } /// Convenience getter for the implicit view. -ui.FlutterView get implicitView => +EngineFlutterWindow get implicitView => EnginePlatformDispatcher.instance.implicitView!; /// Utility function for CanvasKit tests to draw pictures without diff --git a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart index 6bd8bfbed9..80093bb031 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart @@ -53,12 +53,12 @@ class StubPictureRenderer implements PictureRenderer { Map clipRequests = {}; } -class StubFlutterView implements ui.FlutterView { +class StubFlutterView implements EngineFlutterView { @override double get devicePixelRatio => throw UnimplementedError(); @override - ui.Display get display => throw UnimplementedError(); + EngineFlutterDisplay get display => throw UnimplementedError(); @override List get displayFeatures => throw UnimplementedError(); @@ -67,23 +67,23 @@ class StubFlutterView implements ui.FlutterView { ui.GestureSettings get gestureSettings => throw UnimplementedError(); @override - ui.ViewPadding get padding => throw UnimplementedError(); + ViewPadding get padding => throw UnimplementedError(); @override - ui.ViewConstraints get physicalConstraints => throw UnimplementedError(); + ViewConstraints get physicalConstraints => throw UnimplementedError(); @override ui.Size get physicalSize => const ui.Size(1000, 1000); @override - ui.PlatformDispatcher get platformDispatcher => throw UnimplementedError(); + EnginePlatformDispatcher get platformDispatcher => throw UnimplementedError(); @override void render(ui.Scene scene, {ui.Size? size}) { } @override - ui.ViewPadding get systemGestureInsets => throw UnimplementedError(); + ViewPadding get systemGestureInsets => throw UnimplementedError(); @override void updateSemantics(ui.SemanticsUpdate update) { @@ -93,10 +93,60 @@ class StubFlutterView implements ui.FlutterView { int get viewId => throw UnimplementedError(); @override - ui.ViewPadding get viewInsets => throw UnimplementedError(); + ViewPadding get viewInsets => throw UnimplementedError(); @override - ui.ViewPadding get viewPadding => throw UnimplementedError(); + ViewPadding get viewPadding => throw UnimplementedError(); + + @override + ui.Size? debugPhysicalSizeOverride; + + @override + bool isDisposed = false; + + @override + PointerBinding get pointerBinding => throw UnimplementedError(); + + @override + set pointerBinding(_) { + throw UnimplementedError(); + } + + @override + ContextMenu get contextMenu => throw UnimplementedError(); + + @override + void debugForceResize() { + throw UnimplementedError(); + } + + @override + DimensionsProvider get dimensionsProvider => throw UnimplementedError(); + + @override + void dispose() { + throw UnimplementedError(); + } + + @override + DomManager get dom => throw UnimplementedError(); + + @override + EmbeddingStrategy get embeddingStrategy => throw UnimplementedError(); + + @override + MouseCursor get mouseCursor => throw UnimplementedError(); + + @override + Stream get onResize => throw UnimplementedError(); + + @override + void resize(ui.Size newPhysicalSize) { + throw UnimplementedError(); + } + + @override + EngineSemanticsOwner get semantics => throw UnimplementedError(); } void testMain() { diff --git a/engine/src/flutter/lib/web_ui/test/ui/renderer_test.dart b/engine/src/flutter/lib/web_ui/test/ui/renderer_test.dart new file mode 100644 index 0000000000..243557c854 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/ui/renderer_test.dart @@ -0,0 +1,102 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:web_engine_tester/golden_tester.dart'; + +import '../common/test_initialization.dart'; +import 'utils.dart'; + +const ui.Color black = ui.Color(0xFF000000); +const ui.Color red = ui.Color(0xFFFF0000); +const ui.Color green = ui.Color(0xFF00FF00); +const ui.Color blue = ui.Color(0xFF0000FF); + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests(); + + setUp(() { + EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(1.0); + }); + + test('can render into multiple views', () async { + const ui.Rect rect = ui.Rect.fromLTRB(0, 0, 180, 120); + + final host1 = createHostElement(rect); + final view1 = EngineFlutterView(EnginePlatformDispatcher.instance, host1); + EnginePlatformDispatcher.instance.viewManager.registerView(view1); + + final host2 = createHostElement(rect); + final view2 = EngineFlutterView(EnginePlatformDispatcher.instance, host2); + EnginePlatformDispatcher.instance.viewManager.registerView(view2); + + final host3 = createHostElement(rect); + final view3 = EngineFlutterView(EnginePlatformDispatcher.instance, host3); + EnginePlatformDispatcher.instance.viewManager.registerView(view3); + + + await Future.wait([ + renderer.renderScene(paintRect(rect, red), view1), + renderer.renderScene(paintRect(rect, green), view2), + renderer.renderScene(paintRect(rect, blue), view3), + ]); + + await matchGoldenFile( + 'ui_multiview_rects.png', + region: ui.Rect.fromLTRB(0, 0, rect.width, rect.height * 3), + ); + + EnginePlatformDispatcher.instance.viewManager.dispose(); + host1.remove(); + host2.remove(); + host3.remove(); + }, skip: isHtml); // HTML renderer doesn't support multi-view. +} + +DomElement createHostElement(ui.Rect rect) { + final host = createDomElement('div'); + host.style + ..width = '${rect.width}px' + ..height = '${rect.height}px'; + domDocument.body!.append(host); + return host; +} + +ui.Scene paintRect(ui.Rect rect, ui.Color color) { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder, rect); + + // Leave some padding. + rect = rect.deflate(5.0); + + // Draw a black border of 5px thickness. + canvas.drawRect( + rect, + ui.Paint() + ..color = black + ..style = ui.PaintingStyle.fill, + ); + rect = rect.deflate(5.0); + + // Fill the inner rect with the given color. + canvas.drawRect( + rect, + ui.Paint() + ..color = color + ..style = ui.PaintingStyle.fill, + ); + + final picture = recorder.endRecording(); + final sb = ui.SceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(ui.Offset.zero, picture); + return sb.build(); +}