diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index a5e9425c0027c..146363a482e14 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -17,18 +17,17 @@ import 'embedded_views_diff.dart'; import 'path.dart'; import 'picture.dart'; import 'picture_recorder.dart'; +import 'rasterizer.dart'; import 'render_canvas.dart'; import 'render_canvas_factory.dart'; -import 'renderer.dart'; /// This composites HTML views into the [ui.Scene]. class HtmlViewEmbedder { - HtmlViewEmbedder._(); + HtmlViewEmbedder(this.sceneHost, this.rasterizer, this.renderCanvasFactory); - /// The [HtmlViewEmbedder] singleton. - static HtmlViewEmbedder instance = HtmlViewEmbedder._(); - - DomElement get skiaSceneHost => CanvasKitRenderer.instance.sceneHost!; + final DomElement sceneHost; + final Rasterizer rasterizer; + final RenderCanvasFactory renderCanvasFactory; /// The context for the current frame. EmbedderFrameContext _context = EmbedderFrameContext(); @@ -219,7 +218,7 @@ class HtmlViewEmbedder { // If the chain was previously attached, attach it to the same position. if (headClipViewWasAttached) { - skiaSceneHost.insertBefore(head, headClipViewNextSibling); + sceneHost.insertBefore(head, headClipViewNextSibling); } return head; } @@ -359,7 +358,7 @@ class HtmlViewEmbedder { } _svgPathDefs = kSvgResourceHeader.cloneNode(false) as SVGElement; _svgPathDefs!.append(createSVGDefsElement()..id = 'sk_path_defs'); - skiaSceneHost.append(_svgPathDefs!); + sceneHost.append(_svgPathDefs!); } void submitFrame() { @@ -386,8 +385,7 @@ class HtmlViewEmbedder { _context.pictureRecorders[pictureRecorderIndex].endRecording()); pictureRecorderIndex++; } - CanvasKitRenderer.instance.rasterizer - .rasterizeToCanvas(overlay, pictures); + rasterizer.rasterizeToCanvas(overlay, pictures); } for (final CkPictureRecorder recorder in _context.pictureRecordersCreatedDuringPreroll) { @@ -439,18 +437,18 @@ class HtmlViewEmbedder { if (diffResult.addToBeginning) { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; - skiaSceneHost.insertBefore(platformViewRoot, elementToInsertBefore); + sceneHost.insertBefore(platformViewRoot, elementToInsertBefore); final RenderCanvas? overlay = _overlays[viewId]; if (overlay != null) { - skiaSceneHost.insertBefore( + sceneHost.insertBefore( overlay.htmlElement, elementToInsertBefore); } } else { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; - skiaSceneHost.append(platformViewRoot); + sceneHost.append(platformViewRoot); final RenderCanvas? overlay = _overlays[viewId]; if (overlay != null) { - skiaSceneHost.append(overlay.htmlElement); + sceneHost.append(overlay.htmlElement); } } } @@ -463,17 +461,17 @@ class HtmlViewEmbedder { if (!overlayElement.isConnected!) { // This overlay wasn't added to the DOM. if (i == _compositionOrder.length - 1) { - skiaSceneHost.append(overlayElement); + sceneHost.append(overlayElement); } else { final int nextView = _compositionOrder[i + 1]; final DomElement nextElement = _viewClipChains[nextView]!.root; - skiaSceneHost.insertBefore(overlayElement, nextElement); + sceneHost.insertBefore(overlayElement, nextElement); } } } } } else { - RenderCanvasFactory.instance.removeSurfacesFromDom(); + renderCanvasFactory.removeSurfacesFromDom(); for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; @@ -492,9 +490,9 @@ class HtmlViewEmbedder { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; final RenderCanvas? overlay = _overlays[viewId]; - skiaSceneHost.append(platformViewRoot); + sceneHost.append(platformViewRoot); if (overlay != null) { - skiaSceneHost.append(overlay.htmlElement); + sceneHost.append(overlay.htmlElement); } _activeCompositionOrder.add(viewId); unusedViews.remove(viewId); @@ -528,7 +526,7 @@ class HtmlViewEmbedder { void _releaseOverlay(int viewId) { if (_overlays[viewId] != null) { final RenderCanvas overlay = _overlays[viewId]!; - RenderCanvasFactory.instance.releaseCanvas(overlay); + renderCanvasFactory.releaseCanvas(overlay); _overlays.remove(viewId); } } @@ -568,7 +566,7 @@ class HtmlViewEmbedder { if (diffResult == null) { // Everything is going to be explicitly recomposited anyway. Release all // the surfaces and assign an overlay to all the surfaces needing one. - RenderCanvasFactory.instance.releaseCanvases(); + renderCanvasFactory.releaseCanvases(); _overlays.clear(); viewsNeedingOverlays.forEach(_initializeOverlay); } else { @@ -638,7 +636,7 @@ class HtmlViewEmbedder { assert(!_overlays.containsKey(viewId)); // Try reusing a cached overlay created for another platform view. - final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas(); + final RenderCanvas overlay = renderCanvasFactory.getCanvas(); _overlays[viewId] = overlay; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index 668afeeb47434..242118a27b784 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -11,7 +11,7 @@ import 'canvas.dart'; import 'canvaskit_api.dart'; import 'image.dart'; import 'native_memory.dart'; -import 'render_canvas_factory.dart'; +import 'renderer.dart'; import 'surface.dart'; /// Implements [ui.Picture] on top of [SkPicture]. @@ -99,7 +99,7 @@ class CkPicture implements ScenePicture { CkImage toImageSync(int width, int height) { assert(debugCheckNotDisposed('Cannot convert picture to image.')); - final Surface surface = RenderCanvasFactory.instance.pictureToImageSurface; + final Surface surface = CanvasKitRenderer.instance.pictureToImageSurface; final CkSurface ckSurface = surface .createOrUpdateSurface(ui.Size(width.toDouble(), height.toDouble())); final CkCanvas ckCanvas = ckSurface.getCanvas(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 8d150ecae4144..6138955d301c4 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -2,80 +2,54 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:meta/meta.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -/// A class that can rasterize [LayerTree]s into a given [Surface]. +/// A class that can rasterize [LayerTree]s into a given `sceneHost` element. class Rasterizer { + Rasterizer(this.sceneHost); + + final DomElement sceneHost; final CompositorContext context = CompositorContext(); - final List _postFrameCallbacks = []; + final RenderCanvasFactory renderCanvasFactory = RenderCanvasFactory(); + late final HtmlViewEmbedder viewEmbedder = + HtmlViewEmbedder(sceneHost, this, renderCanvasFactory); - /// This is an SkSurface backed by an OffScreenCanvas. This single Surface is - /// used to render to many RenderCanvases to produce the rendered scene. - final Surface _offscreenSurface = Surface(); ui.Size _currentFrameSize = ui.Size.zero; /// Render the given [pictures] so it is displayed by the given [canvas]. Future rasterizeToCanvas( RenderCanvas canvas, List pictures) async { - await _offscreenSurface.rasterizeToCanvas( - _currentFrameSize, canvas, pictures); + await CanvasKitRenderer.instance.offscreenSurface.rasterizeToCanvas( + _currentFrameSize, + canvas, + pictures, + ); } - /// Sets the maximum size of the Skia resource cache, in bytes. - void setSkiaResourceCacheMaxBytes(int bytes) => - _offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); - /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. void draw(LayerTree layerTree) { - try { - if (layerTree.frameSize.isEmpty) { - // Available drawing area is empty. Skip drawing. - return; - } - - _currentFrameSize = layerTree.frameSize; - _offscreenSurface.acquireFrame(_currentFrameSize); - HtmlViewEmbedder.instance.frameSize = _currentFrameSize; - final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize); - pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); - final Frame compositorFrame = context.acquireFrame( - pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance); - - compositorFrame.raster(layerTree, ignoreRasterCache: true); - - CanvasKitRenderer.instance.sceneHost! - .prepend(RenderCanvasFactory.instance.baseCanvas.htmlElement); - rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas, - [pictureRecorder.endRecording()]); - - HtmlViewEmbedder.instance.submitFrame(); - } finally { - _runPostFrameCallbacks(); + if (layerTree.frameSize.isEmpty) { + // Available drawing area is empty. Skip drawing. + return; } - } - void addPostFrameCallback(ui.VoidCallback callback) { - _postFrameCallbacks.add(callback); - } + _currentFrameSize = layerTree.frameSize; + CanvasKitRenderer.instance.offscreenSurface.acquireFrame(_currentFrameSize); + viewEmbedder.frameSize = _currentFrameSize; + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize); + pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); + final Frame compositorFrame = context.acquireFrame( + pictureRecorder.recordingCanvas!, viewEmbedder); - void _runPostFrameCallbacks() { - for (int i = 0; i < _postFrameCallbacks.length; i++) { - final ui.VoidCallback callback = _postFrameCallbacks[i]; - callback(); - } - for (int i = 0; i < frameReferences.length; i++) { - frameReferences[i].value = null; - } - frameReferences.clear(); - } + compositorFrame.raster(layerTree, ignoreRasterCache: true); + + sceneHost.prepend(renderCanvasFactory.baseCanvas.htmlElement); + rasterizeToCanvas(renderCanvasFactory.baseCanvas, + [pictureRecorder.endRecording()]); - /// Forces the post-frame callbacks to run. Useful in tests. - @visibleForTesting - void debugRunPostFrameCallbacks() { - _runPostFrameCallbacks(); + viewEmbedder.submitFrame(); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart index 593390972377c..387b63156524b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart @@ -14,35 +14,11 @@ class RenderCanvasFactory { }()); } - /// The lazy-initialized singleton surface factory. - /// - /// [debugClear] causes this singleton to be reinitialized. - static RenderCanvasFactory get instance => - _instance ??= RenderCanvasFactory(); - - /// Returns the raw (potentially uninitialized) value of the singleton. - /// - /// Useful in tests for checking the lifecycle of this class. - static RenderCanvasFactory? get debugUninitializedInstance => _instance; - - // Override the current instance with a new one. - // - // This should only be used in tests. - static void debugSetInstance(RenderCanvasFactory newInstance) { - _instance = newInstance; - } - - static RenderCanvasFactory? _instance; - /// The base canvas to paint on. This is the default canvas which will be /// painted to. If there are no platform views, then this canvas will render /// the entire scene. final RenderCanvas baseCanvas = RenderCanvas(); - /// A surface used specifically for `Picture.toImage` when software rendering - /// is supported. - late final Surface pictureToImageSurface = Surface(); - /// Canvases created by this factory which are currently in use. final List _liveCanvases = []; @@ -137,6 +113,5 @@ class RenderCanvasFactory { baseCanvas.dispose(); _liveCanvases.clear(); _cache.clear(); - _instance = null; } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index a9ea464da00cf..ec4db4399cbfb 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -44,9 +44,16 @@ class CanvasKitRenderer implements Renderer { DomElement? _sceneHost; DomElement? get sceneHost => _sceneHost; - late Rasterizer rasterizer = Rasterizer(); + /// This is an SkSurface backed by an OffScreenCanvas. This single Surface is + /// used to render to many RenderCanvases to produce the rendered scene. + final Surface offscreenSurface = Surface(); - set resourceCacheMaxBytes(int bytes) => rasterizer.setSkiaResourceCacheMaxBytes(bytes); + set resourceCacheMaxBytes(int bytes) => + offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); + + /// A surface used specifically for `Picture.toImage` when software rendering + /// is supported. + final Surface pictureToImageSurface = Surface(); @override Future initialize() async { @@ -64,13 +71,7 @@ class CanvasKitRenderer implements Renderer { @override void reset(FlutterViewEmbedder embedder) { - // CanvasKit uses a static scene element that never gets replaced, so it's - // added eagerly during initialization here and never touched, unless the - // system is reset due to hot restart or in a test. - _sceneHost = createDomElement('flt-scene'); - // TODO(harryterkelsen): Do this operation on the appropriate Flutter View. - final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; - implicitView.dom.setScene(_sceneHost!); + // No work required. } @override @@ -373,7 +374,7 @@ class CanvasKitRenderer implements Renderer { CkParagraphBuilder(style); @override - void renderScene(ui.Scene scene) { + void renderScene(ui.Scene scene, ui.FlutterView view) { // "Build finish" and "raster start" happen back-to-back because we // render on the same thread, so there's no overhead from hopping to // another thread. @@ -384,10 +385,39 @@ class CanvasKitRenderer implements Renderer { frameTimingsOnBuildFinish(); frameTimingsOnRasterStart(); + // TODO(harryterkelsen): Use `FlutterViewManager.onViewsChanged` to manage + // the lifecycle of Rasterizers, + // https://github.com/flutter/flutter/issues/137073. + final Rasterizer rasterizer = + _getRasterizerForView(view as EngineFlutterView); + rasterizer.draw((scene as LayerScene).layerTree); frameTimingsOnRasterFinish(); } + final Map _rasterizers = + {}; + + Rasterizer _getRasterizerForView(EngineFlutterView view) { + return _rasterizers.putIfAbsent(view, () { + return Rasterizer(view.dom.sceneHost); + }); + } + + /// Returns the [Rasterizer] that has been created for the given [view]. + /// Used in tests. + Rasterizer debugGetRasterizerForView(EngineFlutterView view) { + return _getRasterizerForView(view); + } + + /// Resets the state of the renderer. Used in tests. + void debugClear() { + for (final Rasterizer rasterizer in _rasterizers.values) { + rasterizer.renderCanvasFactory.debugClear(); + rasterizer.viewEmbedder.debugClear(); + } + } + @override void clearFragmentProgramCache() { _programs.clear(); diff --git a/lib/web_ui/lib/src/engine/html/renderer.dart b/lib/web_ui/lib/src/engine/html/renderer.dart index 5d5598f639dbf..7e74faefc09a0 100644 --- a/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/lib/web_ui/lib/src/engine/html/renderer.dart @@ -323,7 +323,7 @@ class HtmlRenderer implements Renderer { CanvasParagraphBuilder(style as EngineParagraphStyle); @override - void renderScene(ui.Scene scene) { + void renderScene(ui.Scene scene, ui.FlutterView view) { final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!); frameTimingsOnRasterFinish(); diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 00782b2e1906a..2f573ed08ac5b 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -32,7 +32,8 @@ class HighContrastSupport { final List _listeners = []; /// Reference to css media query that indicates whether high contrast is on. - final DomMediaQueryList _highContrastMediaQuery = domWindow.matchMedia(_highContrastMediaQueryString); + final DomMediaQueryList _highContrastMediaQuery = + domWindow.matchMedia(_highContrastMediaQueryString); late final DomEventListener _onHighContrastChangeListener = createDomEventListener(_onHighContrastChange); @@ -168,7 +169,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// * [PlatformDisptacher.views] for a list of all [FlutterView]s provided /// by the platform. @override - EngineFlutterWindow? get implicitView => viewManager[kImplicitViewId] as EngineFlutterWindow?; + EngineFlutterWindow? get implicitView => + viewManager[kImplicitViewId] as EngineFlutterWindow?; /// A callback that is invoked whenever the platform's [devicePixelRatio], /// [physicalSize], [padding], [viewInsets], or [systemGestureInsets] @@ -464,7 +466,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { } switch (name) { - /// This should be in sync with shell/common/shell.cc case 'flutter/skia': final MethodCall decoded = jsonCodec.decodeMethodCall(data); @@ -476,7 +477,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { 'Argument to Skia.setResourceCacheMaxBytes must be an int, but was ${decoded.arguments.runtimeType}', ); final int cacheSizeInBytes = decoded.arguments as int; - CanvasKitRenderer.instance.resourceCacheMaxBytes = cacheSizeInBytes; + CanvasKitRenderer.instance.resourceCacheMaxBytes = + cacheSizeInBytes; } // Also respond in HTML mode. Otherwise, apps would have to detect @@ -506,33 +508,43 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { case 'HapticFeedback.vibrate': final String? type = decoded.arguments as String?; vibrate(_getHapticFeedbackDuration(type)); - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setApplicationSwitcherDescription': - final Map arguments = decoded.arguments as Map; + final Map arguments = + decoded.arguments as Map; final String label = arguments['label'] as String? ?? ''; // TODO(web): Stop setting the color from here, https://github.com/flutter/flutter/issues/123365 - final int primaryColor = arguments['primaryColor'] as int? ?? 0xFF000000; + final int primaryColor = + arguments['primaryColor'] as int? ?? 0xFF000000; domDocument.title = label; setThemeColor(ui.Color(primaryColor)); - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setSystemUIOverlayStyle': - final Map arguments = decoded.arguments as Map; + final Map arguments = + decoded.arguments as Map; final int? statusBarColor = arguments['statusBarColor'] as int?; - setThemeColor(statusBarColor == null ? null : ui.Color(statusBarColor)); - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + setThemeColor( + statusBarColor == null ? null : ui.Color(statusBarColor)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setPreferredOrientations': final List arguments = decoded.arguments as List; - ScreenOrientation.instance.setPreferredOrientation(arguments).then((bool success) { + ScreenOrientation.instance + .setPreferredOrientation(arguments) + .then((bool success) { replyToPlatformMessage( callback, jsonCodec.encodeSuccessEnvelope(success)); }); return; case 'SystemSound.play': // There are no default system sounds on web. - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'Clipboard.setData': ClipboardMessageHandler().setDataMethodCall(decoded, callback); @@ -559,21 +571,25 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { switch (decoded.method) { case 'enableContextMenu': implicitView!.contextMenu.enable(); - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'disableContextMenu': implicitView!.contextMenu.disable(); - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); return; } return; case 'flutter/mousecursor': final MethodCall decoded = standardCodec.decodeMethodCall(data); - final Map arguments = decoded.arguments as Map; + final Map arguments = + decoded.arguments as Map; switch (decoded.method) { case 'activateSystemCursor': - implicitView!.mouseCursor.activateSystemCursor(arguments.tryString('kind')); + implicitView!.mouseCursor + .activateSystemCursor(arguments.tryString('kind')); } return; @@ -585,14 +601,18 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { return; case 'flutter/platform_views': - final MethodCall(:String method, :dynamic arguments) = standardCodec.decodeMethodCall(data); + final MethodCall(:String method, :dynamic arguments) = + standardCodec.decodeMethodCall(data); final int? flutterViewId = tryViewId(arguments); if (flutterViewId == null) { - implicitView!.platformViewMessageHandler.handleLegacyPlatformViewCall(method, arguments, callback!); + implicitView!.platformViewMessageHandler + .handleLegacyPlatformViewCall(method, arguments, callback!); return; } arguments as Map; - viewManager[flutterViewId]!.platformViewMessageHandler.handlePlatformViewCall(method, arguments, callback!); + viewManager[flutterViewId]! + .platformViewMessageHandler + .handlePlatformViewCall(method, arguments, callback!); return; case 'flutter/accessibility': @@ -610,7 +630,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // supported. implicitView!.handleNavigationMessage(data).then((bool handled) { if (handled) { - replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); + replyToPlatformMessage( + callback, jsonCodec.encodeSuccessEnvelope(true)); } else { callback?.call(null); } @@ -634,9 +655,11 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { replyToPlatformMessage(callback, null); } - Future _handleFlutterAssetsMessage(String url, ui.PlatformMessageResponseCallback? callback) async { + Future _handleFlutterAssetsMessage( + String url, ui.PlatformMessageResponseCallback? callback) async { try { - final HttpFetchResponse response = await ui_web.assetManager.loadAsset(url) as HttpFetchResponse; + final HttpFetchResponse response = + await ui_web.assetManager.loadAsset(url) as HttpFetchResponse; final ByteBuffer assetData = await response.asByteBuffer(); replyToPlatformMessage(callback, assetData.asByteData()); } catch (error) { @@ -707,7 +730,13 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// painting. @override void render(ui.Scene scene, [ui.FlutterView? view]) { - renderer.renderScene(scene); + assert(view != null || implicitView != null, + 'Calling render without a FlutterView'); + if (view == null && implicitView == null) { + // If there is no view to render into, then this is a no-op. + return; + } + renderer.renderScene(scene, view ?? implicitView!); } /// Additional accessibility features that may be enabled by the platform. @@ -800,11 +829,11 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { } updateLocales(); // First time, for good measure. _onLocaleChangedSubscription = - DomSubscription(domWindow, 'languagechange', (DomEvent _) { - // Update internal config, then propagate the changes. - updateLocales(); - invokeOnLocaleChanged(); - }); + DomSubscription(domWindow, 'languagechange', (DomEvent _) { + // Update internal config, then propagate the changes. + updateLocales(); + invokeOnLocaleChanged(); + }); } /// Removes the [_onLocaleChangedSubscription]. @@ -934,8 +963,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { void _addFontSizeObserver() { const String styleAttribute = 'style'; - _fontSizeObserver = createDomMutationObserver( - (JSArray mutations, DomMutationObserver _) { + _fontSizeObserver = + createDomMutationObserver((JSArray mutations, DomMutationObserver _) { for (final JSAny? mutation in mutations.toDart) { final DomMutationRecord record = mutation! as DomMutationRecord; if (record.type == 'attributes' && @@ -993,7 +1022,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { void updateSemanticsEnabled(bool semanticsEnabled) { if (semanticsEnabled != this.semanticsEnabled) { - configuration = configuration.copyWith(semanticsEnabled: semanticsEnabled); + configuration = + configuration.copyWith(semanticsEnabled: semanticsEnabled); if (_onSemanticsEnabledChanged != null) { invokeOnSemanticsEnabledChanged(); } @@ -1047,8 +1077,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { : ui.Brightness.light); _brightnessMediaQueryListener = createDomEventListener((DomEvent event) { - final DomMediaQueryListEvent mqEvent = - event as DomMediaQueryListEvent; + final DomMediaQueryListEvent mqEvent = event as DomMediaQueryListEvent; _updatePlatformBrightness( mqEvent.matches! ? ui.Brightness.dark : ui.Brightness.light); }); @@ -1097,8 +1126,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// observe when this callback is invoked. @override - ui.VoidCallback? get onSystemFontFamilyChanged => - _onSystemFontFamilyChanged; + ui.VoidCallback? get onSystemFontFamilyChanged => _onSystemFontFamilyChanged; ui.VoidCallback? _onSystemFontFamilyChanged; Zone? _onSystemFontFamilyChangedZone; @override @@ -1150,7 +1178,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// The framework invokes this callback in the same zone in which the /// callback was set. @override - ui.SemanticsActionEventCallback? get onSemanticsActionEvent => _onSemanticsActionEvent; + ui.SemanticsActionEventCallback? get onSemanticsActionEvent => + _onSemanticsActionEvent; ui.SemanticsActionEventCallback? _onSemanticsActionEvent; Zone _onSemanticsActionEventZone = Zone.root; @override @@ -1164,12 +1193,14 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { void invokeOnSemanticsAction( int nodeId, ui.SemanticsAction action, ByteData? args) { invoke1( - _onSemanticsActionEvent, _onSemanticsActionEventZone, ui.SemanticsActionEvent( - type: action, - nodeId: nodeId, - viewId: 0, // TODO(goderbauer): Wire up the real view ID. - arguments: args, - ), + _onSemanticsActionEvent, + _onSemanticsActionEventZone, + ui.SemanticsActionEvent( + type: action, + nodeId: nodeId, + viewId: 0, // TODO(goderbauer): Wire up the real view ID. + arguments: args, + ), ); } @@ -1245,7 +1276,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { ui.FrameData get frameData => const ui.FrameData.webOnly(); @override - double scaleFontSize(double unscaledFontSize) => unscaledFontSize * textScaleFactor; + double scaleFontSize(double unscaledFontSize) => + unscaledFontSize * textScaleFactor; } bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData? data) { @@ -1331,7 +1363,8 @@ const double _defaultRootFontSize = 16.0; /// Finds the text scale factor of the browser by looking at the computed style /// of the browser's element. double findBrowserTextScaleFactor() { - final num fontSize = parseFontSize(domDocument.documentElement!) ?? _defaultRootFontSize; + final num fontSize = + parseFontSize(domDocument.documentElement!) ?? _defaultRootFontSize; return fontSize / _defaultRootFontSize; } @@ -1411,8 +1444,10 @@ class PlatformConfiguration { String? systemFontFamily, }) { return PlatformConfiguration( - accessibilityFeatures: accessibilityFeatures ?? this.accessibilityFeatures, - alwaysUse24HourFormat: alwaysUse24HourFormat ?? this.alwaysUse24HourFormat, + accessibilityFeatures: + accessibilityFeatures ?? this.accessibilityFeatures, + alwaysUse24HourFormat: + alwaysUse24HourFormat ?? this.alwaysUse24HourFormat, semanticsEnabled: semanticsEnabled ?? this.semanticsEnabled, platformBrightness: platformBrightness ?? this.platformBrightness, textScaleFactor: textScaleFactor ?? this.textScaleFactor, diff --git a/lib/web_ui/lib/src/engine/renderer.dart b/lib/web_ui/lib/src/engine/renderer.dart index 0f4982de62eba..9f39fcff9ea94 100644 --- a/lib/web_ui/lib/src/engine/renderer.dart +++ b/lib/web_ui/lib/src/engine/renderer.dart @@ -222,5 +222,5 @@ abstract class Renderer { ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style); - FutureOr renderScene(ui.Scene scene); + FutureOr renderScene(ui.Scene scene, ui.FlutterView view); } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index edcd0a7e19b1f..292994ec18122 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -398,8 +398,11 @@ class SkwasmRenderer implements Renderer { return decoder; } + // TODO(harryterkelsen): Add multiview support, + // https://github.com/flutter/flutter/issues/137073. @override - Future renderScene(ui.Scene scene) => sceneView.renderScene(scene as EngineScene); + Future renderScene(ui.Scene scene, ui.FlutterView view) => + sceneView.renderScene(scene as EngineScene); @override String get rendererTag => 'skwasm'; diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart index 3964590b3e70a..fe63d34ad0462 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart @@ -150,7 +150,7 @@ class SkwasmRenderer implements Renderer { } @override - void renderScene(ui.Scene scene) { + void renderScene(ui.Scene scene, ui.FlutterView view) { throw UnimplementedError('Skwasm not implemented on this platform.'); } diff --git a/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart b/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart index ead5723743d18..cb0b178e0554f 100644 --- a/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart +++ b/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart @@ -49,7 +49,7 @@ void testMain() { builder.pushOffset(0, 0); builder.addPicture(ui.Offset.zero, checkerboard); builder.pushBackdropFilter(ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)); - CanvasKitRenderer.instance.rasterizer.draw(builder.build().layerTree); + CanvasKitRenderer.instance.renderScene(builder.build(), implicitView); await matchGoldenFile('canvaskit_backdropfilter_blur_edges.png', region: region); }); diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart index 46a436db87726..3ada138eb3722 100644 --- a/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -146,25 +146,24 @@ void testMain() { final LayerSceneBuilder builder = LayerSceneBuilder(); builder.pushOffset(0, 0); builder.addPicture(ui.Offset.zero, picture); - final LayerTree layerTree = builder.build().layerTree; - CanvasKitRenderer.instance.rasterizer.draw(layerTree); + final LayerScene scene = builder.build(); + CanvasKitRenderer.instance.renderScene(scene, implicitView); // Now draw an empty layer tree and confirm that the red rectangle is // no longer drawn. final LayerSceneBuilder emptySceneBuilder = LayerSceneBuilder(); emptySceneBuilder.pushOffset(0, 0); - final LayerTree emptyLayerTree = emptySceneBuilder.build().layerTree; - CanvasKitRenderer.instance.rasterizer.draw(emptyLayerTree); + final LayerScene emptyScene = emptySceneBuilder.build(); + CanvasKitRenderer.instance.renderScene(emptyScene, implicitView); await matchGoldenFile('canvaskit_empty_scene.png', region: const ui.Rect.fromLTRB(0, 0, 100, 100)); }); // Regression test for https://github.com/flutter/flutter/issues/121758 - test('resources used in temporary surfaces for Image.toByteData can cross to rendering overlays', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - RenderCanvasFactory.instance.debugClear(); - + test( + 'resources used in temporary surfaces for Image.toByteData can cross to rendering overlays', + () async { ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -212,7 +211,7 @@ void testMain() { sb.pop(); // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); await matchGoldenFile('cross_overlay_resources.png', region: const ui.Rect.fromLTRB(0, 0, 100, 100)); }); diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart index b98359512ce23..d11dc2e04c871 100644 --- a/lib/web_ui/test/canvaskit/common.dart +++ b/lib/web_ui/test/canvaskit/common.dart @@ -23,18 +23,19 @@ void setUpCanvasKitTest() { ); tearDown(() { - HtmlViewEmbedder.instance.debugClear(); - RenderCanvasFactory.instance.debugClear(); + CanvasKitRenderer.instance.debugClear(); }); - setUp(() => - renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride - = 'assets/fallback_fonts/'); - tearDown(() => - renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride - = null); + setUp(() => renderer.fontCollection.fontFallbackManager!.downloadQueue + .fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/'); + tearDown(() => renderer.fontCollection.fontFallbackManager!.downloadQueue + .fallbackFontUrlPrefixOverride = null); } +/// Convenience getter for the implicit view. +ui.FlutterView get implicitView => + EnginePlatformDispatcher.instance.implicitView!; + /// Utility function for CanvasKit tests to draw pictures without /// the [CkPictureRecorder] boilerplate. CkPicture paintPicture( @@ -45,10 +46,15 @@ CkPicture paintPicture( return recorder.endRecording(); } -Future matchSceneGolden(String goldenFile, LayerScene scene, { +Future matchSceneGolden( + String goldenFile, + LayerScene scene, { required ui.Rect region, }) async { - CanvasKitRenderer.instance.rasterizer.draw(scene.layerTree); + // TODO(harryterkelsen): Enforce the render rule. Render can only be called in + // the scope of `onBeginFrame` or `onDrawFrame`, + // https://github.com/flutter/flutter/issues/137073. + CanvasKitRenderer.instance.renderScene(scene, implicitView); await matchGoldenFile(goldenFile, region: region); } @@ -61,7 +67,7 @@ Future matchPictureGolden(String goldenFile, CkPicture picture, final LayerSceneBuilder sb = LayerSceneBuilder(); sb.pushOffset(0, 0); sb.addPicture(ui.Offset.zero, picture); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); await matchGoldenFile(goldenFile, region: region); } @@ -69,7 +75,8 @@ Future matchImage(ui.Image left, ui.Image right) async { if (left.width != right.width || left.height != right.height) { return false; } - int getPixel(ByteData data, int x, int y) => data.getUint32((x + y * left.width) * 4); + int getPixel(ByteData data, int x, int y) => + data.getUint32((x + y * left.width) * 4); final ByteData leftData = (await left.toByteData())!; final ByteData rightData = (await right.toByteData())!; for (int y = 0; y < left.height; y++) { @@ -113,7 +120,8 @@ Future disposePlatformView(int id) { /// Creates a pre-laid out one-line paragraph of text. /// /// Useful in tests that need a simple label to annotate goldens. -CkParagraph makeSimpleText(String text, { +CkParagraph makeSimpleText( + String text, { String? fontFamily, double? fontSize, ui.FontStyle? fontStyle, diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index bd945fe8b5f4d..7884b85aeb8a8 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -17,7 +17,7 @@ EngineFlutterWindow get implicitView => EnginePlatformDispatcher.instance.implicitView!; DomElement get platformViewsHost => implicitView.dom.platformViewsHost; -DomElement get sceneElement => implicitView.dom.sceneHost.querySelector('flt-scene')!; +DomElement get sceneHost => implicitView.dom.sceneHost; void main() { internalBootstrapBrowserTest(() => testMain); @@ -32,7 +32,6 @@ void testMain() { }); test('embeds interactive platform views', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -42,13 +41,13 @@ void testMain() { final LayerSceneBuilder sb = LayerSceneBuilder(); sb.pushOffset(0, 0); sb.addPlatformView(0, width: 10, height: 10); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); // The platform view is now split in two parts. The contents live // as a child of the glassPane, and the slot lives in the glassPane // shadow root. The slot is the one that has pointer events auto. final DomElement contents = platformViewsHost.querySelector('#view-0')!; - final DomElement slot = sceneElement.querySelector('slot')!; + final DomElement slot = sceneHost.querySelector('slot')!; final DomElement contentsHost = contents.parent!; final DomElement slotHost = slot.parent!; @@ -65,7 +64,6 @@ void testMain() { }); test('clips platform views with RRects', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -77,14 +75,14 @@ void testMain() { sb.pushClipRRect( ui.RRect.fromLTRBR(0, 0, 10, 10, const ui.Radius.circular(3))); sb.addPlatformView(0, width: 10, height: 10); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); expect( - sceneElement.querySelectorAll('#sk_path_defs').single, + sceneHost.querySelectorAll('#sk_path_defs').single, isNotNull, ); expect( - sceneElement + sceneHost .querySelectorAll('#sk_path_defs') .single .querySelectorAll('clipPath') @@ -92,21 +90,20 @@ void testMain() { isNotNull, ); expect( - sceneElement.querySelectorAll('flt-clip').single.style.clipPath, + sceneHost.querySelectorAll('flt-clip').single.style.clipPath, 'url("#svgClip1")', ); expect( - sceneElement.querySelectorAll('flt-clip').single.style.width, + sceneHost.querySelectorAll('flt-clip').single.style.width, '100%', ); expect( - sceneElement.querySelectorAll('flt-clip').single.style.height, + sceneHost.querySelectorAll('flt-clip').single.style.height, '100%', ); }); test('correctly transforms platform views', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -121,11 +118,11 @@ void testMain() { sb.pushTransform(scaleMatrix.toFloat64()); sb.pushOffset(3, 3); sb.addPlatformView(0, width: 10, height: 10); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); // Transformations happen on the slot element. final DomElement slotHost = - sceneElement.querySelector('flt-platform-view-slot')!; + sceneHost.querySelector('flt-platform-view-slot')!; expect( slotHost.style.transform, @@ -145,10 +142,10 @@ void testMain() { final LayerSceneBuilder sb = LayerSceneBuilder(); sb.addPlatformView(0, offset: const ui.Offset(3, 4), width: 5, height: 6); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); final DomElement slotHost = - sceneElement.querySelector('flt-platform-view-slot')!; + sceneHost.querySelector('flt-platform-view-slot')!; final DomCSSStyleDeclaration style = slotHost.style; expect(style.transform, 'matrix(1, 0, 0, 1, 3, 4)'); @@ -163,11 +160,13 @@ void testMain() { }); // Returns the list of CSS transforms applied to the ancestor chain of - // elements starting from `viewHost`, up until and excluding . + // elements starting from `viewHost`, up until and excluding + // . List getTransformChain(DomElement viewHost) { final List chain = []; DomElement? element = viewHost; - while (element != null && element.tagName.toLowerCase() != 'flt-scene') { + while (element != null && + element.tagName.toLowerCase() != 'flt-scene-host') { chain.add(element.style.transform); element = element.parent; } @@ -187,11 +186,11 @@ void testMain() { sb.pushOffset(6, 6); sb.addPlatformView(0, width: 10, height: 10); sb.pop(); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); // Transformations happen on the slot element. DomElement slotHost = - sceneElement.querySelector('flt-platform-view-slot')!; + sceneHost.querySelector('flt-platform-view-slot')!; expect( getTransformChain(slotHost), @@ -208,10 +207,10 @@ void testMain() { sb.pushClipRect(ui.Rect.largest); sb.pushOffset(9, 9); sb.addPlatformView(0, width: 10, height: 10); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); // Transformations happen on the slot element. - slotHost = sceneElement.querySelector('flt-platform-view-slot')!; + slotHost = sceneHost.querySelector('flt-platform-view-slot')!; expect( getTransformChain(slotHost), @@ -236,11 +235,11 @@ void testMain() { sb.pushOffset(2, 2); sb.pushOffset(3, 3); sb.addPlatformView(0, width: 10, height: 10); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); // Transformations happen on the slot element. final DomElement slotHost = - sceneElement.querySelector('flt-platform-view-slot')!; + sceneHost.querySelector('flt-platform-view-slot')!; expect( getTransformChain(slotHost), @@ -263,11 +262,11 @@ void testMain() { sb.pushClipRect(ui.Rect.largest); sb.pushOffset(9, 9); sb.addPlatformView(0, width: 10, height: 10); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); // Transformations happen on the slot element. final DomElement slotHost = - sceneElement.querySelector('flt-platform-view-slot')!; + sceneHost.querySelector('flt-platform-view-slot')!; expect( getTransformChain(slotHost), @@ -280,7 +279,6 @@ void testMain() { }); test('renders overlays on top of platform views', () async { - expect(RenderCanvasFactory.instance.debugCacheSize, 0); final CkPicture testPicture = paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) { canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint()); @@ -304,7 +302,7 @@ void testMain() { sb.addPicture(ui.Offset.zero, testPicture); sb.addPlatformView(i, width: 10, height: 10); } - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); } // Frame 1: @@ -457,7 +455,7 @@ void testMain() { sb.addPicture(ui.Offset.zero, testPicture); sb.addPlatformView(view, width: 10, height: 10); } - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); } // Frame 1: @@ -576,7 +574,7 @@ void testMain() { LayerSceneBuilder sb = LayerSceneBuilder(); sb.pushOffset(0, 0); sb.addPlatformView(0, width: 10, height: 10); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -589,7 +587,7 @@ void testMain() { sb = LayerSceneBuilder(); sb.pushOffset(0, 0); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, @@ -625,7 +623,7 @@ void testMain() { implicitView.debugPhysicalSizeOverride = const ui.Size(100, 100); implicitView.debugForceResize(); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -634,7 +632,7 @@ void testMain() { implicitView.debugPhysicalSizeOverride = const ui.Size(200, 200); implicitView.debugForceResize(); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -647,7 +645,6 @@ void testMain() { }, skip: isSafari || isFirefox); test('removed the DOM node of an unrendered platform view', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -657,7 +654,7 @@ void testMain() { LayerSceneBuilder sb = LayerSceneBuilder(); sb.pushOffset(0, 0); sb.addPlatformView(0, width: 10, height: 10); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -671,7 +668,7 @@ void testMain() { sb = LayerSceneBuilder(); sb.pushOffset(0, 0); sb.addPlatformView(1, width: 10, height: 10); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -687,7 +684,7 @@ void testMain() { // the platform view. sb = LayerSceneBuilder(); sb.pushOffset(0, 0); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, ]); @@ -703,7 +700,6 @@ void testMain() { test( 'removes old SVG clip definitions from the DOM when the view is recomposited', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'test-view', @@ -716,10 +712,10 @@ void testMain() { sb.pushClipRRect( ui.RRect.fromLTRBR(0, 0, 10, 10, const ui.Radius.circular(3))); sb.addPlatformView(0, width: 10, height: 10); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); } - final DomNode skPathDefs = sceneElement.querySelector('#sk_path_defs')!; + final DomNode skPathDefs = sceneHost.querySelector('#sk_path_defs')!; expect(skPathDefs.childNodes, hasLength(0)); @@ -737,7 +733,6 @@ void testMain() { test('does not crash when a prerolled platform view is not composited', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -750,14 +745,13 @@ void testMain() { sb.addPlatformView(0, width: 10, height: 10); sb.pop(); // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, ]); }); test('does not create overlays for invisible platform views', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( 'test-visible-view', (int viewId) => @@ -783,7 +777,7 @@ void testMain() { sb.pushOffset(0, 0); sb.addPlatformView(1, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -794,7 +788,7 @@ void testMain() { sb.addPlatformView(0, width: 10, height: 10); sb.addPlatformView(1, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -808,7 +802,7 @@ void testMain() { sb.addPlatformView(1, width: 10, height: 10); sb.addPlatformView(2, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -827,7 +821,7 @@ void testMain() { sb.addPlatformView(2, width: 10, height: 10); sb.addPlatformView(3, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -846,7 +840,7 @@ void testMain() { sb.addPlatformView(3, width: 10, height: 10); sb.addPlatformView(4, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -867,7 +861,7 @@ void testMain() { sb.addPlatformView(4, width: 10, height: 10); sb.addPlatformView(5, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -890,7 +884,7 @@ void testMain() { sb.addPlatformView(5, width: 10, height: 10); sb.addPlatformView(6, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -912,7 +906,7 @@ void testMain() { sb.addPlatformView(5, width: 10, height: 10); sb.addPlatformView(6, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -931,7 +925,7 @@ void testMain() { sb.addPlatformView(3, width: 10, height: 10); sb.addPlatformView(4, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -948,7 +942,7 @@ void testMain() { sb.addPlatformView(2, width: 10, height: 10); sb.addPlatformView(1, width: 10, height: 10); sb.pop(); - rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -982,7 +976,7 @@ void _expectSceneMatches( String? reason, }) { // Convert the scene elements to its corresponding array of _EmbeddedViewMarker - final List<_EmbeddedViewMarker> sceneElements = sceneElement.children + final List<_EmbeddedViewMarker> sceneElements = sceneHost.children .where((DomElement element) => element.tagName != 'svg') .map((DomElement element) => _tagToViewMarker[element.tagName.toLowerCase()]!) diff --git a/lib/web_ui/test/canvaskit/image_golden_test.dart b/lib/web_ui/test/canvaskit/image_golden_test.dart index 6e8fd738d9137..a662b25ea0bfd 100644 --- a/lib/web_ui/test/canvaskit/image_golden_test.dart +++ b/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -637,7 +637,7 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { canvas.drawImage(snapshot, ui.Offset.zero, CkPaint()); sb.addPicture(ui.Offset.zero, recorder.endRecording()); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); await matchGoldenFile( 'canvaskit_read_back_decoded_image_$mode.png', region: const ui.Rect.fromLTRB(0, 0, 150, 150), @@ -687,7 +687,7 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { canvas.drawParagraph(makeSimpleText('2'), const ui.Offset(2, 2)); sb.addPicture(ui.Offset.zero, recorder.endRecording()); } - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); await matchGoldenFile( 'canvaskit_cross_gl_context_image_$mode.png', region: const ui.Rect.fromLTRB(0, 0, 100, 100), @@ -731,7 +731,7 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { canvas.restore(); sb.addPicture(ui.Offset.zero, recorder.endRecording()); } - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); await matchGoldenFile( 'canvaskit_picture_texture_toimage.png', region: const ui.Rect.fromLTRB(0, 0, 128, 128), @@ -774,7 +774,7 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { canvas.drawImage(snapshot, ui.Offset.zero, CkPaint()); sb.addPicture(ui.Offset.zero, recorder.endRecording()); - CanvasKitRenderer.instance.rasterizer.draw(sb.build().layerTree); + CanvasKitRenderer.instance.renderScene(sb.build(), implicitView); await matchGoldenFile( 'canvaskit_read_back_decoded_image_$mode.png', region: const ui.Rect.fromLTRB(0, 0, 150, 150), diff --git a/lib/web_ui/test/canvaskit/layer_test.dart b/lib/web_ui/test/canvaskit/layer_test.dart index 9be900f29e5d2..cf83e8abee889 100644 --- a/lib/web_ui/test/canvaskit/layer_test.dart +++ b/lib/web_ui/test/canvaskit/layer_test.dart @@ -37,8 +37,9 @@ void testMain() { )); sb.addPicture(ui.Offset.zero, picture); - final LayerTree layerTree = sb.build().layerTree; - CanvasKitRenderer.instance.rasterizer.draw(layerTree); + final LayerScene scene = sb.build(); + final LayerTree layerTree = scene.layerTree; + CanvasKitRenderer.instance.renderScene(scene, implicitView); final ClipRectEngineLayer clipRect = layerTree.rootLayer.debugLayers.single as ClipRectEngineLayer; expect(clipRect.paintBounds, const ui.Rect.fromLTRB(15, 15, 30, 30)); @@ -92,8 +93,9 @@ void testMain() { ); sb.addPicture(ui.Offset.zero, picture); - final LayerTree layerTree = sb.build().layerTree; - CanvasKitRenderer.instance.rasterizer.draw(layerTree); + final LayerScene scene = sb.build(); + final LayerTree layerTree = scene.layerTree; + CanvasKitRenderer.instance.renderScene(scene, implicitView); final ImageFilterEngineLayer imageFilterLayer = layerTree.rootLayer.debugLayers.single as ImageFilterEngineLayer; diff --git a/lib/web_ui/test/canvaskit/multi_view_test.dart b/lib/web_ui/test/canvaskit/multi_view_test.dart new file mode 100644 index 0000000000000..2aa1815b67ea6 --- /dev/null +++ b/lib/web_ui/test/canvaskit/multi_view_test.dart @@ -0,0 +1,38 @@ +// 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 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit', () { + setUpCanvasKitTest(); + + test('can render into arbitrary views', () async { + final CkPicture picture = + paintPicture(const ui.Rect.fromLTRB(0, 0, 60, 60), (CkCanvas canvas) { + canvas.drawRect(const ui.Rect.fromLTRB(0, 0, 60, 60), + CkPaint()..style = ui.PaintingStyle.fill); + }); + + final LayerSceneBuilder sb = LayerSceneBuilder(); + + sb.addPicture(ui.Offset.zero, picture); + final LayerScene scene = sb.build(); + CanvasKitRenderer.instance.renderScene(scene, implicitView); + + final EngineFlutterView anotherView = EngineFlutterView( + EnginePlatformDispatcher.instance, createDomElement('another-view')); + CanvasKitRenderer.instance.renderScene(scene, anotherView); + }); + }); +} diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart index 70aa4e6073ffa..2e85655a19825 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart @@ -67,11 +67,15 @@ void testMain() { expect(canvas.canvasElement.isConnected, isFalse); } - final RenderCanvasFactory originalFactory = RenderCanvasFactory.instance; - expect(RenderCanvasFactory.debugUninitializedInstance, isNotNull); + final EngineFlutterView implicitView = + EnginePlatformDispatcher.instance.implicitView!; + + final RenderCanvasFactory originalFactory = CanvasKitRenderer.instance + .debugGetRasterizerForView(implicitView) + .renderCanvasFactory; // Cause the surface and its canvas to be attached to the page - CanvasKitRenderer.instance.sceneHost! + implicitView.dom.sceneHost .prepend(originalFactory.baseCanvas.htmlElement); expect(originalFactory.baseCanvas.canvasElement.isConnected, isTrue); @@ -79,14 +83,13 @@ void testMain() { final List overlays = []; for (int i = 0; i < 3; i++) { final RenderCanvas canvas = originalFactory.getCanvas(); - CanvasKitRenderer.instance.sceneHost!.prepend(canvas.htmlElement); + implicitView.dom.sceneHost.prepend(canvas.htmlElement); overlays.add(canvas); } expect(originalFactory.debugSurfaceCount, 4); // Trigger hot restart clean-up logic and check that we indeed clean up. debugEmulateHotRestart(); - expect(RenderCanvasFactory.debugUninitializedInstance, isNull); expectDisposed(originalFactory.baseCanvas); overlays.forEach(expectDisposed); expect(originalFactory.debugSurfaceCount, 1); diff --git a/lib/web_ui/test/ui/platform_view_test.dart b/lib/web_ui/test/ui/platform_view_test.dart index 7a16696aa2ca5..7d0b01c2fbb82 100644 --- a/lib/web_ui/test/ui/platform_view_test.dart +++ b/lib/web_ui/test/ui/platform_view_test.dart @@ -12,6 +12,7 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import 'package:web_engine_tester/golden_tester.dart'; import '../common/test_initialization.dart'; +import 'utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -64,7 +65,7 @@ Future testMain() async { width: 50, height: 50, ); - await renderer.renderScene(sb.build()); + await renderer.renderScene(sb.build(), implicitView); await matchGoldenFile('picture_platformview_overlap.png', region: region); }); @@ -96,7 +97,7 @@ Future testMain() async { ); sb.addPicture(const ui.Offset(125, 125), picture); - await renderer.renderScene(sb.build()); + await renderer.renderScene(sb.build(), implicitView); await matchGoldenFile('picture_platformview_sandwich.png', region: region); }); @@ -125,7 +126,7 @@ Future testMain() async { width: 50, height: 50, ); - await renderer.renderScene(sb.build()); + await renderer.renderScene(sb.build(), implicitView); await matchGoldenFile('platformview_transformed.png', region: region); }); @@ -154,7 +155,7 @@ Future testMain() async { width: 50, height: 50, ); - await renderer.renderScene(sb.build()); + await renderer.renderScene(sb.build(), implicitView); await matchGoldenFile('platformview_opacity.png', region: region); }); diff --git a/lib/web_ui/test/ui/scene_builder_test.dart b/lib/web_ui/test/ui/scene_builder_test.dart index 741a8f3816860..6ecc2f33f896e 100644 --- a/lib/web_ui/test/ui/scene_builder_test.dart +++ b/lib/web_ui/test/ui/scene_builder_test.dart @@ -36,7 +36,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_centered_circle.png', region: region); }); @@ -60,7 +60,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_rotated_rounded_square.png', region: region); }); @@ -75,7 +75,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_circle_clip_rect.png', region: region); }); @@ -93,7 +93,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_circle_clip_rrect.png', region: region); }); @@ -109,7 +109,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_rectangle_clip_circular_path.png', region: region); }); @@ -137,7 +137,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_opacity_circles_on_square.png', region: region); }); @@ -177,7 +177,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_shader_mask.png', region: region); }, skip: isFirefox && isHtml); // https://github.com/flutter/flutter/issues/86623 @@ -209,7 +209,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_backdrop_filter.png', region: region); }); @@ -228,7 +228,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_image_filter.png', region: region); }); @@ -250,7 +250,7 @@ Future testMain() async { ); })); - await renderer.renderScene(sceneBuilder.build()); + await renderer.renderScene(sceneBuilder.build(), implicitView); await matchGoldenFile('scene_builder_color_filter.png', region: region); }); }); diff --git a/lib/web_ui/test/ui/utils.dart b/lib/web_ui/test/ui/utils.dart index 38f976ad91f3f..24d8c148f240e 100644 --- a/lib/web_ui/test/ui/utils.dart +++ b/lib/web_ui/test/ui/utils.dart @@ -5,7 +5,8 @@ import 'dart:async'; import 'package:ui/src/engine.dart'; -import 'package:ui/src/engine/skwasm/skwasm_stub.dart' if (dart.library.ffi) 'package:ui/src/engine/skwasm/skwasm_impl.dart'; +import 'package:ui/src/engine/skwasm/skwasm_stub.dart' + if (dart.library.ffi) 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart'; Picture drawPicture(void Function(Canvas) drawCommands) { @@ -20,9 +21,12 @@ Future drawPictureUsingCurrentRenderer(Picture picture) async { final SceneBuilder sb = SceneBuilder(); sb.pushOffset(0, 0); sb.addPicture(Offset.zero, picture); - await renderer.renderScene(sb.build()); + await renderer.renderScene(sb.build(), implicitView); } +/// Convenience getter for the implicit view. +FlutterView get implicitView => EnginePlatformDispatcher.instance.implicitView!; + /// Returns [true] if this test is running in the CanvasKit renderer. bool get isCanvasKit => renderer is CanvasKitRenderer;