[web] Multi-view support for Skwasm (flutter/engine#48893)

Support multi-view mode in the Skwasm renderer.

Fixes https://github.com/flutter/flutter/issues/138925
This commit is contained in:
Mouad Debbar 2024-08-23 11:20:07 -04:00 committed by GitHub
parent f2d6dc0a45
commit ff452d94a3
9 changed files with 174 additions and 28 deletions

View File

@ -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<void> renderScene(ui.Scene scene, ui.FlutterView view) async {
Future<void> 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]!;

View File

@ -302,7 +302,7 @@ class HtmlRenderer implements Renderer {
CanvasParagraphBuilder(style as EngineParagraphStyle);
@override
Future<void> renderScene(ui.Scene scene, ui.FlutterView view) async {
Future<void> renderScene(ui.Scene scene, EngineFlutterView view) async {
final EngineFlutterView implicitView =
EnginePlatformDispatcher.instance.implicitView!;
scene as SurfaceScene;

View File

@ -225,5 +225,5 @@ abstract class Renderer {
ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style);
Future<void> renderScene(ui.Scene scene, ui.FlutterView view);
Future<void> renderScene(ui.Scene scene, EngineFlutterView view);
}

View File

@ -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<SliceContainer> containers = <SliceContainer>[];

View File

@ -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<EngineFlutterView, EngineSceneView> _sceneViews = <EngineFlutterView, EngineSceneView>{};
@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<void> renderScene(ui.Scene scene, ui.FlutterView view) {
Future<void> 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

View File

@ -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

View File

@ -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

View File

@ -53,12 +53,12 @@ class StubPictureRenderer implements PictureRenderer {
Map<ScenePicture, ui.Rect> clipRequests = <ScenePicture, ui.Rect>{};
}
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<ui.DisplayFeature> 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<ui.Size?> get onResize => throw UnimplementedError();
@override
void resize(ui.Size newPhysicalSize) {
throw UnimplementedError();
}
@override
EngineSemanticsOwner get semantics => throw UnimplementedError();
}
void testMain() {

View File

@ -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<void> 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();
}