diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 78b87fec6793f..ed92e13fd2a57 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -386,6 +386,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/color_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/embedded_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/engine_delegate.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/image.dart @@ -394,6 +395,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/initialization.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index d9f9dca215784..a45179ec73f17 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -27,6 +27,7 @@ part 'engine/color_filter.dart'; part 'engine/compositor/canvas.dart'; part 'engine/compositor/canvas_kit_canvas.dart'; part 'engine/compositor/color_filter.dart'; +part 'engine/compositor/embedded_views.dart'; part 'engine/compositor/engine_delegate.dart'; part 'engine/compositor/fonts.dart'; part 'engine/compositor/image.dart'; @@ -35,6 +36,7 @@ part 'engine/compositor/initialization.dart'; part 'engine/compositor/layer.dart'; part 'engine/compositor/layer_scene_builder.dart'; part 'engine/compositor/layer_tree.dart'; +part 'engine/compositor/n_way_canvas.dart'; part 'engine/compositor/path.dart'; part 'engine/compositor/path_metrics.dart'; part 'engine/compositor/picture.dart'; diff --git a/lib/web_ui/lib/src/engine/compositor/canvas.dart b/lib/web_ui/lib/src/engine/compositor/canvas.dart index 1f765793900e2..67dbbe6520ae8 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvas.dart @@ -12,6 +12,10 @@ class SkCanvas { int get saveCount => skCanvas.callMethod('getSaveCount'); + void clear(ui.Color color) { + skCanvas.callMethod('clear', [color.value]); + } + void clipPath(ui.Path path, bool doAntiAlias) { final SkPath skPath = path; final js.JsObject intersectClipOp = canvasKit['ClipOp']['Intersect']; diff --git a/lib/web_ui/lib/src/engine/compositor/embedded_views.dart b/lib/web_ui/lib/src/engine/compositor/embedded_views.dart new file mode 100644 index 0000000000000..798a3d7dc8476 --- /dev/null +++ b/lib/web_ui/lib/src/engine/compositor/embedded_views.dart @@ -0,0 +1,544 @@ +// 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. + +part of engine; + +/// This composites HTML views into the [ui.Scene]. +class HtmlViewEmbedder { + /// A picture recorder associated with a view id. + /// + /// When we composite in the platform view, we need to create a new canvas + /// for further paint commands to paint to, since the composited view will + /// be on top of the current canvas, and we want further paint commands to + /// be on top of the platform view. + final Map _pictureRecorders = + {}; + + /// The most recent composition parameters for a given view id. + /// + /// If we receive a request to composite a view, but the composition + /// parameters haven't changed, we can avoid having to recompute the + /// element stack that correctly composites the view into the scene. + final Map _currentCompositionParams = + {}; + + /// The HTML element associated with the given view id. + final Map _views = {}; + + /// The root view in the stack of mutator elements for the view id. + final Map _rootViews = {}; + + /// The overlay for the view id. + final Map _overlays = {}; + + /// The views that need to be recomposited into the scene on the next frame. + final Set _viewsToRecomposite = {}; + + /// The views that need to be disposed of on the next frame. + final Set _viewsToDispose = {}; + + /// The list of view ids that should be composited, in order. + List _compositionOrder = []; + + /// The most recent composition order. + List _activeCompositionOrder = []; + + /// The number of clipping elements used last time the view was composited. + Map _clipCount = {}; + + /// The size of the frame, in physical pixels. + ui.Size _frameSize; + + void set frameSize(ui.Size size) { + if (_frameSize == size) { + return; + } + _activeCompositionOrder.clear(); + _frameSize = size; + } + + void handlePlatformViewCall( + ByteData data, + ui.PlatformMessageResponseCallback callback, + ) { + const MethodCodec codec = StandardMethodCodec(); + final MethodCall decoded = codec.decodeMethodCall(data); + + switch (decoded.method) { + case 'create': + _create(decoded, callback); + return; + case 'dispose': + _dispose(decoded, callback); + return; + } + callback(null); + } + + void _create( + MethodCall methodCall, ui.PlatformMessageResponseCallback callback) { + final Map args = methodCall.arguments; + final int viewId = args['id']; + final String viewType = args['viewType']; + const MethodCodec codec = StandardMethodCodec(); + + if (_views[viewId] != null) { + callback(codec.encodeErrorEnvelope( + code: 'recreating_view', + message: 'trying to create an already created view', + details: 'view id: $viewId', + )); + return; + } + + final PlatformViewFactory factory = + platformViewRegistry.registeredFactories[viewType]; + if (factory == null) { + callback(codec.encodeErrorEnvelope( + code: 'unregistered_view_type', + message: 'trying to create a view with an unregistered type', + details: 'unregistered view type: $viewType', + )); + return; + } + + // TODO(het): Support creation parameters. + html.Element embeddedView = factory(viewId); + _views[viewId] = embeddedView; + + _rootViews[viewId] = embeddedView; + + callback(codec.encodeSuccessEnvelope(null)); + } + + void _dispose( + MethodCall methodCall, ui.PlatformMessageResponseCallback callback) { + int viewId = methodCall.arguments; + const MethodCodec codec = StandardMethodCodec(); + if (!_views.containsKey(viewId)) { + callback(codec.encodeErrorEnvelope( + code: 'unknown_view', + message: 'trying to dispose an unknown view', + details: 'view id: $viewId', + )); + } + _viewsToDispose.add(viewId); + callback(codec.encodeSuccessEnvelope(null)); + } + + List getCurrentCanvases() { + final List canvases = []; + for (int i = 0; i < _compositionOrder.length; i++) { + final int viewId = _compositionOrder[i]; + canvases.add(_pictureRecorders[viewId].recordingCanvas); + } + return canvases; + } + + void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) { + final pictureRecorder = SkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & _frameSize); + pictureRecorder.recordingCanvas.clear(ui.Color(0x00000000)); + _pictureRecorders[viewId] = pictureRecorder; + _compositionOrder.add(viewId); + + // Do nothing if the params didn't change. + if (_currentCompositionParams[viewId] == params) { + return; + } + _currentCompositionParams[viewId] = params; + _viewsToRecomposite.add(viewId); + } + + SkCanvas compositeEmbeddedView(int viewId) { + // Do nothing if this view doesn't need to be composited. + if (!_viewsToRecomposite.contains(viewId)) { + return _pictureRecorders[viewId].recordingCanvas; + } + _compositeWithParams(viewId, _currentCompositionParams[viewId]); + _viewsToRecomposite.remove(viewId); + return _pictureRecorders[viewId].recordingCanvas; + } + + void _compositeWithParams(int viewId, EmbeddedViewParams params) { + final html.Element platformView = _views[viewId]; + platformView.style.width = '${params.size.width}px'; + platformView.style.height = '${params.size.height}px'; + platformView.style.position = 'absolute'; + + final int currentClippingCount = _countClips(params.mutators); + final int previousClippingCount = _clipCount[viewId]; + if (currentClippingCount != previousClippingCount) { + _clipCount[viewId] = currentClippingCount; + html.Element oldPlatformViewRoot = _rootViews[viewId]; + html.Element newPlatformViewRoot = _reconstructClipViewsChain( + currentClippingCount, + platformView, + oldPlatformViewRoot, + ); + _rootViews[viewId] = newPlatformViewRoot; + } + _applyMutators(params.mutators, platformView); + } + + int _countClips(MutatorsStack mutators) { + int clipCount = 0; + for (final Mutator mutator in mutators) { + if (mutator.isClipType) { + clipCount++; + } + } + return clipCount; + } + + html.Element _reconstructClipViewsChain( + int numClips, + html.Element platformView, + html.Element headClipView, + ) { + int indexInFlutterView = -1; + if (headClipView.parent != null) { + indexInFlutterView = skiaSceneHost.children.indexOf(headClipView); + headClipView.remove(); + } + html.Element head = platformView; + int clipIndex = 0; + // Re-use as much existing clip views as needed. + while (head != headClipView && clipIndex < numClips) { + head = head.parent; + clipIndex++; + } + // If there weren't enough existing clip views, add more. + while (clipIndex < numClips) { + html.Element clippingView = html.Element.tag('flt-clip'); + clippingView.append(head); + head = clippingView; + clipIndex++; + } + head.remove(); + + // If the chain was previously attached, attach it to the same position. + if (indexInFlutterView > -1) { + skiaSceneHost.children.insert(indexInFlutterView, head); + } + return head; + } + + void _applyMutators(MutatorsStack mutators, html.Element embeddedView) { + html.Element head = embeddedView; + Matrix4 headTransform = Matrix4.identity(); + double embeddedOpacity = 1.0; + _resetAnchor(head); + + for (final Mutator mutator in mutators) { + switch (mutator.type) { + case MutatorType.transform: + headTransform.multiply(mutator.matrix); + head.style.transform = + float64ListToCssTransform(headTransform.storage); + break; + case MutatorType.clipRect: + case MutatorType.clipRRect: + case MutatorType.clipPath: + html.Element clipView = head.parent; + clipView.style.clip = ''; + clipView.style.clipPath = ''; + headTransform = Matrix4.identity(); + clipView.style.transform = ''; + if (mutator.rect != null) { + final ui.Rect rect = mutator.rect; + clipView.style.clip = 'rect(${rect.top}px, ${rect.right}px, ' + '${rect.bottom}px, ${rect.left}px)'; + } else if (mutator.rrect != null) { + final SkPath path = SkPath(); + path.addRRect(mutator.rrect); + _ensureSvgPathDefs(); + html.Element pathDefs = _svgPathDefs.querySelector('#sk_path_defs'); + _clipPathCount += 1; + html.Element newClipPath = + html.Element.html('' + '' + ''); + pathDefs.append(newClipPath); + clipView.style.clipPath = 'url(#svgClip$_clipPathCount)'; + } else if (mutator.path != null) { + final SkPath path = mutator.path; + _ensureSvgPathDefs(); + html.Element pathDefs = _svgPathDefs.querySelector('#sk_path_defs'); + _clipPathCount += 1; + html.Element newClipPath = + html.Element.html('' + '' + ''); + pathDefs.append(newClipPath); + clipView.style.clipPath = 'url(#svgClip$_clipPathCount)'; + } + _resetAnchor(clipView); + head = clipView; + break; + case MutatorType.opacity: + embeddedOpacity *= mutator.alphaFloat; + break; + } + } + + embeddedView.style.opacity = embeddedOpacity.toString(); + + // Reverse scale based on screen scale. + // + // HTML elements use logical (CSS) pixels, but we have been using physical + // pixels, so scale down the head element to match the logical resolution. + final double scale = html.window.devicePixelRatio; + final double inverseScale = 1 / scale; + final Matrix4 scaleMatrix = + Matrix4.diagonal3Values(inverseScale, inverseScale, 1); + headTransform.multiply(scaleMatrix); + head.style.transform = float64ListToCssTransform(headTransform.storage); + } + + /// Sets the transform origin to the top-left corner of the element. + /// + /// By default, the transform origin is the center of the element, but + /// Flutter assumes the transform origin is the top-left point. + void _resetAnchor(html.Element element) { + element.style.transformOrigin = '0 0 0'; + element.style.position = 'absolute'; + } + + int _clipPathCount = 0; + + html.Element _svgPathDefs; + + /// Ensures we add a container of SVG path defs to the DOM so they can + /// be referred to in clip-path: url(#blah). + void _ensureSvgPathDefs() { + if (_svgPathDefs != null) return; + _svgPathDefs = html.Element.html( + '', + treeSanitizer: _NullTreeSanitizer(), + ); + skiaSceneHost.append(_svgPathDefs); + } + + void submitFrame() { + disposeViews(); + + for (int i = 0; i < _compositionOrder.length; i++) { + int viewId = _compositionOrder[i]; + ensureOverlayInitialized(viewId); + final SurfaceFrame frame = + _overlays[viewId].surface.acquireFrame(_frameSize); + final SkCanvas canvas = frame.skiaCanvas; + canvas.drawPicture(_pictureRecorders[viewId].endRecording()); + frame.submit(); + } + _pictureRecorders.clear(); + if (_listEquals(_compositionOrder, _activeCompositionOrder)) { + _compositionOrder.clear(); + return; + } + _activeCompositionOrder.clear(); + + for (int i = 0; i < _compositionOrder.length; i++) { + int viewId = _compositionOrder[i]; + html.Element platformViewRoot = _rootViews[viewId]; + html.Element overlay = _overlays[viewId].surface.htmlElement; + platformViewRoot.remove(); + skiaSceneHost.append(platformViewRoot); + overlay.remove(); + skiaSceneHost.append(overlay); + _activeCompositionOrder.add(viewId); + } + _compositionOrder.clear(); + } + + void disposeViews() { + if (_viewsToDispose.isEmpty) { + return; + } + + for (int viewId in _viewsToDispose) { + final html.Element rootView = _rootViews[viewId]; + rootView.remove(); + _views.remove(viewId); + _rootViews.remove(viewId); + if (_overlays[viewId] != null) { + final Overlay overlay = _overlays[viewId]; + overlay.surface.htmlElement?.remove(); + } + _overlays.remove(viewId); + _currentCompositionParams.remove(viewId); + _clipCount.remove(viewId); + _viewsToRecomposite.remove(viewId); + } + _viewsToDispose.clear(); + } + + void ensureOverlayInitialized(int viewId) { + Overlay overlay = _overlays[viewId]; + if (overlay != null) { + return; + } + Surface surface = Surface(); + SkSurface skSurface = surface.acquireRenderSurface(_frameSize); + _overlays[viewId] = Overlay(surface, skSurface); + } +} + +/// The parameters passed to the view embedder. +class EmbeddedViewParams { + EmbeddedViewParams(this.offset, this.size, MutatorsStack mutators) + : mutators = MutatorsStack._copy(mutators); + + final ui.Offset offset; + final ui.Size size; + final MutatorsStack mutators; + + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! EmbeddedViewParams) return false; + + EmbeddedViewParams typedOther = other; + return offset == typedOther.offset && + size == typedOther.size && + mutators == typedOther.mutators; + } + + int get hashCode => ui.hashValues(offset, size, mutators); +} + +enum MutatorType { + clipRect, + clipRRect, + clipPath, + transform, + opacity, +} + +/// Stores mutation information like clipping or transform. +class Mutator { + const Mutator._( + this.type, + this.rect, + this.rrect, + this.path, + this.matrix, + this.alpha, + ); + + final MutatorType type; + final ui.Rect rect; + final ui.RRect rrect; + final ui.Path path; + final Matrix4 matrix; + final int alpha; + + const Mutator.clipRect(ui.Rect rect) + : this._(MutatorType.clipRect, rect, null, null, null, null); + const Mutator.clipRRect(ui.RRect rrect) + : this._(MutatorType.clipRRect, null, rrect, null, null, null); + const Mutator.clipPath(ui.Path path) + : this._(MutatorType.clipPath, null, null, path, null, null); + const Mutator.transform(Matrix4 matrix) + : this._(MutatorType.transform, null, null, null, matrix, null); + const Mutator.opacity(int alpha) + : this._(MutatorType.opacity, null, null, null, null, alpha); + + bool get isClipType => + type == MutatorType.clipRect || + type == MutatorType.clipRRect || + type == MutatorType.clipPath; + + double get alphaFloat => alpha / 255.0; + + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! Mutator) return false; + + final Mutator typedOther = other; + if (type != typedOther.type) { + return false; + } + + switch (type) { + case MutatorType.clipRect: + return rect == typedOther.rect; + case MutatorType.clipRRect: + return rrect == typedOther.rrect; + case MutatorType.clipPath: + return path == typedOther.path; + case MutatorType.transform: + return matrix == typedOther.matrix; + case MutatorType.opacity: + return alpha == typedOther.alpha; + } + } + + int get hashCode => ui.hashValues(type, rect, rrect, path, matrix, alpha); +} + +/// A stack of mutators that can be applied to an embedded view. +class MutatorsStack extends Iterable { + MutatorsStack() : _mutators = []; + + MutatorsStack._copy(MutatorsStack original) + : _mutators = List.from(original._mutators); + + final List _mutators; + + void pushClipRect(ui.Rect rect) { + _mutators.add(Mutator.clipRect(rect)); + } + + void pushClipRRect(ui.RRect rrect) { + _mutators.add(Mutator.clipRRect(rrect)); + } + + void pushClipPath(ui.Path path) { + _mutators.add(Mutator.clipPath(path)); + } + + void pushTransform(Matrix4 matrix) { + _mutators.add(Mutator.transform(matrix)); + } + + void pushOpacity(int alpha) { + _mutators.add(Mutator.opacity(alpha)); + } + + void pop() { + _mutators.removeLast(); + } + + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! MutatorsStack) return false; + + final MutatorsStack typedOther = other; + if (_mutators.length != typedOther._mutators.length) { + return false; + } + + for (int i = 0; i < _mutators.length; i++) { + if (_mutators[i] != typedOther._mutators[i]) { + return false; + } + } + + return true; + } + + int get hashCode => ui.hashList(_mutators); + + @override + Iterator get iterator => _mutators.reversed.iterator; +} + +/// Represents a surface overlaying a platform view. +class Overlay { + final Surface surface; + final SkSurface skSurface; + + Overlay(this.surface, this.skSurface); +} diff --git a/lib/web_ui/lib/src/engine/compositor/engine_delegate.dart b/lib/web_ui/lib/src/engine/compositor/engine_delegate.dart index bf28ebbfb91eb..0a854696ca9c7 100644 --- a/lib/web_ui/lib/src/engine/compositor/engine_delegate.dart +++ b/lib/web_ui/lib/src/engine/compositor/engine_delegate.dart @@ -58,6 +58,7 @@ class Engine extends RuntimeDelegate { } layerTree.frameSize = frameSize; + layerTree.devicePixelRatio = _viewportMetrics.devicePixelRatio; _animator.render(layerTree); } diff --git a/lib/web_ui/lib/src/engine/compositor/initialization.dart b/lib/web_ui/lib/src/engine/compositor/initialization.dart index 9f043e15cf827..9ede38968e7e1 100644 --- a/lib/web_ui/lib/src/engine/compositor/initialization.dart +++ b/lib/web_ui/lib/src/engine/compositor/initialization.dart @@ -32,6 +32,10 @@ Future initializeSkia() { }, ]); }); + + /// Add a Skia scene host. + skiaSceneHost = html.Element.tag('flt-scene'); + domRenderer.renderScene(skiaSceneHost); return canvasKitCompleter.future; } @@ -42,3 +46,6 @@ js.JsObject canvasKit; /// The Skia font collection. SkiaFontCollection skiaFontCollection; + +/// The scene host, where the root canvas and overlay canvases are added to. +html.Element skiaSceneHost; diff --git a/lib/web_ui/lib/src/engine/compositor/layer.dart b/lib/web_ui/lib/src/engine/compositor/layer.dart index 3321bbe5dd6f8..8656ee8907b0e 100644 --- a/lib/web_ui/lib/src/engine/compositor/layer.dart +++ b/lib/web_ui/lib/src/engine/compositor/layer.dart @@ -34,18 +34,36 @@ class PrerollContext { /// A raster cache. Used to register candidates for caching. final RasterCache rasterCache; - PrerollContext(this.rasterCache); + /// A compositor for embedded HTML views. + final HtmlViewEmbedder viewEmbedder; + + final MutatorsStack mutatorsStack = MutatorsStack(); + + PrerollContext(this.rasterCache, this.viewEmbedder); } /// A context shared by all layers during the paint pass. class PaintContext { - /// The canvas to paint to. - final SkCanvas canvas; + /// A multi-canvas that applies clips, transforms, and opacity + /// operations to all canvases (root canvas and overlay canvases for the + /// platform views). + SkNWayCanvas internalNodesCanvas; + + /// The canvas for leaf nodes to paint to. + SkCanvas leafNodesCanvas; /// A raster cache potentially containing pre-rendered pictures. final RasterCache rasterCache; - PaintContext(this.canvas, this.rasterCache); + /// A compositor for embedded HTML views. + final HtmlViewEmbedder viewEmbedder; + + PaintContext( + this.internalNodesCanvas, + this.leafNodesCanvas, + this.rasterCache, + this.viewEmbedder, + ); } /// A layer that contains child layers. @@ -100,9 +118,9 @@ class BackdropFilterLayer extends ContainerLayer { @override void paint(PaintContext context) { - context.canvas.saveLayerWithFilter(paintBounds, _filter); + context.internalNodesCanvas.saveLayerWithFilter(paintBounds, _filter); paintChildren(context); - context.canvas.restore(); + context.internalNodesCanvas.restore(); } } @@ -116,29 +134,31 @@ class ClipPathLayer extends ContainerLayer { : assert(_clipBehavior != ui.Clip.none); @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); + void preroll(PrerollContext context, Matrix4 matrix) { + context.mutatorsStack.pushClipPath(_clipPath); + final ui.Rect childPaintBounds = prerollChildren(context, matrix); final ui.Rect clipBounds = _clipPath.getBounds(); if (childPaintBounds.overlaps(clipBounds)) { paintBounds = childPaintBounds.intersect(clipBounds); } + context.mutatorsStack.pop(); } @override void paint(PaintContext paintContext) { assert(needsPainting); - paintContext.canvas.save(); - paintContext.canvas.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge); + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.saveLayer(paintBounds, null); + paintContext.internalNodesCanvas.saveLayer(paintBounds, null); } paintChildren(paintContext); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } } @@ -152,31 +172,33 @@ class ClipRectLayer extends ContainerLayer { : assert(_clipBehavior != ui.Clip.none); @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); + void preroll(PrerollContext context, Matrix4 matrix) { + context.mutatorsStack.pushClipRect(_clipRect); + final ui.Rect childPaintBounds = prerollChildren(context, matrix); if (childPaintBounds.overlaps(_clipRect)) { paintBounds = childPaintBounds.intersect(_clipRect); } + context.mutatorsStack.pop(); } @override void paint(PaintContext paintContext) { assert(needsPainting); - paintContext.canvas.save(); - paintContext.canvas.clipRect( + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.clipRect( _clipRect, ui.ClipOp.intersect, _clipBehavior != ui.Clip.hardEdge, ); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.saveLayer(_clipRect, null); + paintContext.internalNodesCanvas.saveLayer(_clipRect, null); } paintChildren(paintContext); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } } @@ -190,28 +212,30 @@ class ClipRRectLayer extends ContainerLayer { : assert(_clipBehavior != ui.Clip.none); @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); + void preroll(PrerollContext context, Matrix4 matrix) { + context.mutatorsStack.pushClipRRect(_clipRRect); + final ui.Rect childPaintBounds = prerollChildren(context, matrix); if (childPaintBounds.overlaps(_clipRRect.outerRect)) { paintBounds = childPaintBounds.intersect(_clipRRect.outerRect); } + context.mutatorsStack.pop(); } @override void paint(PaintContext paintContext) { assert(needsPainting); - paintContext.canvas.save(); - paintContext.canvas + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas .clipRRect(_clipRRect, _clipBehavior != ui.Clip.hardEdge); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.saveLayer(paintBounds, null); + paintContext.internalNodesCanvas.saveLayer(paintBounds, null); } paintChildren(paintContext); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } } @@ -223,12 +247,16 @@ class OpacityLayer extends ContainerLayer implements ui.OpacityEngineLayer { OpacityLayer(this._alpha, this._offset); @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { + void preroll(PrerollContext context, Matrix4 matrix) { final Matrix4 childMatrix = Matrix4.copy(matrix); childMatrix.translate(_offset.dx, _offset.dy); - final ui.Rect childPaintBounds = - prerollChildren(prerollContext, childMatrix); - paintBounds = childPaintBounds.translate(_offset.dx, _offset.dy); + context.mutatorsStack + .pushTransform(Matrix4.translationValues(_offset.dx, _offset.dy, 0.0)); + context.mutatorsStack.pushOpacity(_alpha); + super.preroll(context, childMatrix); + context.mutatorsStack.pop(); + context.mutatorsStack.pop(); + paintBounds = paintBounds.translate(_offset.dx, _offset.dy); } @override @@ -238,16 +266,16 @@ class OpacityLayer extends ContainerLayer implements ui.OpacityEngineLayer { final ui.Paint paint = ui.Paint(); paint.color = ui.Color.fromARGB(_alpha, 0, 0, 0); - paintContext.canvas.save(); - paintContext.canvas.translate(_offset.dx, _offset.dy); + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.translate(_offset.dx, _offset.dy); final ui.Rect saveLayerBounds = paintBounds.shift(-_offset); - paintContext.canvas.saveLayer(saveLayerBounds, paint); + paintContext.internalNodesCanvas.saveLayer(saveLayerBounds, paint); paintChildren(paintContext); // Restore twice: once for the translate and once for the saveLayer. - paintContext.canvas.restore(); - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); + paintContext.internalNodesCanvas.restore(); } } @@ -260,11 +288,12 @@ class TransformLayer extends ContainerLayer TransformLayer(this._transform); @override - void preroll(PrerollContext prerollContext, Matrix4 matrix) { + void preroll(PrerollContext context, Matrix4 matrix) { final Matrix4 childMatrix = matrix * _transform; - final ui.Rect childPaintBounds = - prerollChildren(prerollContext, childMatrix); + context.mutatorsStack.pushTransform(_transform); + final ui.Rect childPaintBounds = prerollChildren(context, childMatrix); paintBounds = _transformRect(_transform, childPaintBounds); + context.mutatorsStack.pop(); } /// Applies the given matrix as a perspective transform to the given point. @@ -307,10 +336,10 @@ class TransformLayer extends ContainerLayer void paint(PaintContext paintContext) { assert(needsPainting); - paintContext.canvas.save(); - paintContext.canvas.transform(_transform.storage); + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.transform(_transform.storage); paintChildren(paintContext); - paintContext.canvas.restore(); + paintContext.internalNodesCanvas.restore(); } } @@ -340,11 +369,11 @@ class PictureLayer extends Layer { assert(picture != null); assert(needsPainting); - paintContext.canvas.save(); - paintContext.canvas.translate(offset.dx, offset.dy); + paintContext.leafNodesCanvas.save(); + paintContext.leafNodesCanvas.translate(offset.dx, offset.dy); - paintContext.canvas.drawPicture(picture); - paintContext.canvas.restore(); + paintContext.leafNodesCanvas.drawPicture(picture); + paintContext.leafNodesCanvas.restore(); } } @@ -432,26 +461,26 @@ class PhysicalShapeLayer extends ContainerLayer assert(needsPainting); if (_elevation != 0) { - drawShadow(paintContext.canvas, _path, _shadowColor, _elevation, + drawShadow(paintContext.leafNodesCanvas, _path, _shadowColor, _elevation, _color.alpha != 0xff); } final ui.Paint paint = ui.Paint()..color = _color; if (_clipBehavior != ui.Clip.antiAliasWithSaveLayer) { - paintContext.canvas.drawPath(_path, paint); + paintContext.leafNodesCanvas.drawPath(_path, paint); } - final int saveCount = paintContext.canvas.save(); + final int saveCount = paintContext.internalNodesCanvas.save(); switch (_clipBehavior) { case ui.Clip.hardEdge: - paintContext.canvas.clipPath(_path, false); + paintContext.internalNodesCanvas.clipPath(_path, false); break; case ui.Clip.antiAlias: - paintContext.canvas.clipPath(_path, true); + paintContext.internalNodesCanvas.clipPath(_path, true); break; case ui.Clip.antiAliasWithSaveLayer: - paintContext.canvas.clipPath(_path, true); - paintContext.canvas.saveLayer(paintBounds, null); + paintContext.internalNodesCanvas.clipPath(_path, true); + paintContext.internalNodesCanvas.saveLayer(paintBounds, null); break; case ui.Clip.none: break; @@ -462,12 +491,12 @@ class PhysicalShapeLayer extends ContainerLayer // (https://github.com/flutter/flutter/issues/18057#issue-328003931) // using saveLayer, we have to call drawPaint instead of drawPath as // anti-aliased drawPath will always have such artifacts. - paintContext.canvas.drawPaint(paint); + paintContext.leafNodesCanvas.drawPaint(paint); } paintChildren(paintContext); - paintContext.canvas.restoreToCount(saveCount); + paintContext.internalNodesCanvas.restoreToCount(saveCount); } /// Draws a shadow on the given [canvas] for the given [path]. @@ -479,3 +508,32 @@ class PhysicalShapeLayer extends ContainerLayer canvas.drawShadow(path, color, elevation, transparentOccluder); } } + +/// A layer which renders a platform view (an HTML element in this case). +class PlatformViewLayer extends Layer { + PlatformViewLayer(this.viewId, this.offset, this.width, this.height); + + final int viewId; + final ui.Offset offset; + final double width; + final double height; + + @override + void preroll(PrerollContext context, Matrix4 matrix) { + paintBounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); + context.viewEmbedder.prerollCompositeEmbeddedView( + viewId, + EmbeddedViewParams( + offset, + ui.Size(width, height), + context.mutatorsStack, + ), + ); + } + + @override + void paint(PaintContext context) { + SkCanvas canvas = context.viewEmbedder.compositeEmbeddedView(viewId); + context.leafNodesCanvas = canvas; + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart b/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart index c07fe108351a4..9e25b218ca309 100644 --- a/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart +++ b/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart @@ -73,7 +73,7 @@ class LayerSceneBuilder implements ui.SceneBuilder { double height = 0.0, Object webOnlyPaintedBy, }) { - // TODO(b/128317425): implement addPlatformView. + currentLayer.add(PlatformViewLayer(viewId, offset, width, height)); } @override diff --git a/lib/web_ui/lib/src/engine/compositor/layer_tree.dart b/lib/web_ui/lib/src/engine/compositor/layer_tree.dart index ee428f5947d0f..d595fa98cfc38 100644 --- a/lib/web_ui/lib/src/engine/compositor/layer_tree.dart +++ b/lib/web_ui/lib/src/engine/compositor/layer_tree.dart @@ -12,6 +12,9 @@ class LayerTree { /// The size (in physical pixels) of the frame to paint this layer tree into. ui.Size frameSize; + /// The devicePixelRatio of the frame to paint this layer tree into. + double devicePixelRatio; + /// Performs a preroll phase before painting the layer tree. /// /// In this phase, the paint boundary for each layer is computed and @@ -19,8 +22,10 @@ class LayerTree { /// to raster. If [ignoreRasterCache] is `true`, then there will be no /// attempt to register pictures to cache. void preroll(Frame frame, {bool ignoreRasterCache = false}) { - final PrerollContext context = - PrerollContext(ignoreRasterCache ? null : frame.rasterCache); + final PrerollContext context = PrerollContext( + ignoreRasterCache ? null : frame.rasterCache, + frame.viewEmbedder, + ); rootLayer.preroll(context, Matrix4.identity()); } @@ -29,8 +34,19 @@ class LayerTree { /// If [ignoreRasterCache] is `true`, then the raster cache will /// not be used. void paint(Frame frame, {bool ignoreRasterCache = false}) { + final SkNWayCanvas internalNodesCanvas = SkNWayCanvas(); + internalNodesCanvas.addCanvas(frame.canvas); + final List overlayCanvases = + frame.viewEmbedder.getCurrentCanvases(); + for (int i = 0; i < overlayCanvases.length; i++) { + internalNodesCanvas.addCanvas(overlayCanvases[i]); + } final PaintContext context = PaintContext( - frame.canvas, ignoreRasterCache ? null : frame.rasterCache); + internalNodesCanvas, + frame.canvas, + ignoreRasterCache ? null : frame.rasterCache, + frame.viewEmbedder, + ); if (rootLayer.needsPainting) { rootLayer.paint(context); } @@ -45,7 +61,10 @@ class Frame { /// A cache of pre-rastered pictures. final RasterCache rasterCache; - Frame(this.canvas, this.rasterCache); + /// The platform view embedder. + final HtmlViewEmbedder viewEmbedder; + + Frame(this.canvas, this.rasterCache, this.viewEmbedder); /// Rasterize the given layer tree into this frame. bool raster(LayerTree layerTree, {bool ignoreRasterCache = false}) { @@ -61,7 +80,7 @@ class CompositorContext { RasterCache rasterCache; /// Acquire a frame using this compositor's settings. - Frame acquireFrame(SkCanvas canvas) { - return Frame(canvas, rasterCache); + Frame acquireFrame(SkCanvas canvas, HtmlViewEmbedder viewEmbedder) { + return Frame(canvas, rasterCache, viewEmbedder); } } diff --git a/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart b/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart new file mode 100644 index 0000000000000..7e59da1a1bbda --- /dev/null +++ b/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart @@ -0,0 +1,86 @@ +// 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. + +part of engine; + +/// A virtual canvas that applies operations to multiple canvases at once. +class SkNWayCanvas { + final List _canvases = []; + + void addCanvas(SkCanvas canvas) { + _canvases.add(canvas); + } + + /// Calls [save] on all canvases. + int save() { + int saveCount; + for (int i = 0; i < _canvases.length; i++) { + saveCount = _canvases[i].save(); + } + return saveCount; + } + + /// Calls [saveLayer] on all canvases. + void saveLayer(ui.Rect bounds, ui.Paint paint) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].saveLayer(bounds, paint); + } + } + + /// Calls [saveLayerWithFilter] on all canvases. + void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].saveLayerWithFilter(bounds, filter); + } + } + + /// Calls [restore] on all canvases. + void restore() { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].restore(); + } + } + + /// Calls [restoreToCount] on all canvases. + void restoreToCount(int count) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].restoreToCount(count); + } + } + + /// Calls [translate] on all canvases. + void translate(double dx, double dy) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].translate(dx, dy); + } + } + + /// Calls [transform] on all canvases. + void transform(Float64List matrix) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].transform(matrix); + } + } + + /// Calls [clipPath] on all canvases. + void clipPath(ui.Path path, bool doAntiAlias) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].clipPath(path, doAntiAlias); + } + } + + /// Calls [clipRect] on all canvases. + void clipRect(ui.Rect rect, ui.ClipOp clipOp, bool doAntiAlias) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].clipRect(rect, clipOp, doAntiAlias); + } + } + + /// Calls [clipRRect] on all canvases. + void clipRRect(ui.RRect rrect, bool doAntiAlias) { + for (int i = 0; i < _canvases.length; i++) { + _canvases[i].clipRRect(rrect, doAntiAlias); + } + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/path.dart b/lib/web_ui/lib/src/engine/compositor/path.dart index 4dd9e05c8ec62..9995ffc5d3cdc 100644 --- a/lib/web_ui/lib/src/engine/compositor/path.dart +++ b/lib/web_ui/lib/src/engine/compositor/path.dart @@ -343,4 +343,8 @@ class SkPath implements ui.Path { throw new UnimplementedError( 'webOnlySerializeToCssPaint is not used in the CanvasKit backend.'); } + + String toSvgString() { + return _skPath.callMethod('toSVGString'); + } } diff --git a/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart b/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart index 8490f5d35acf7..68e8cbdf3fe3e 100644 --- a/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart +++ b/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart @@ -7,6 +7,7 @@ part of engine; class SkPictureRecorder implements ui.PictureRecorder { ui.Rect _cullRect; js.JsObject _recorder; + SkCanvas _recordingCanvas; SkCanvas beginRecording(ui.Rect bounds) { _cullRect = bounds; @@ -15,9 +16,12 @@ class SkPictureRecorder implements ui.PictureRecorder { [bounds.left, bounds.top, bounds.right, bounds.bottom]); final js.JsObject skCanvas = _recorder.callMethod('beginRecording', [skRect]); - return SkCanvas(skCanvas); + _recordingCanvas = SkCanvas(skCanvas); + return _recordingCanvas; } + SkCanvas get recordingCanvas => _recordingCanvas; + @override ui.Picture endRecording() { final js.JsObject skPicture = diff --git a/lib/web_ui/lib/src/engine/compositor/rasterizer.dart b/lib/web_ui/lib/src/engine/compositor/rasterizer.dart index 2953623ce504f..66c4b194764ea 100644 --- a/lib/web_ui/lib/src/engine/compositor/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/compositor/rasterizer.dart @@ -8,8 +8,11 @@ part of engine; class Rasterizer { final Surface surface; final CompositorContext context = CompositorContext(); + final HtmlViewEmbedder viewEmbedder = HtmlViewEmbedder(); - Rasterizer(this.surface); + Rasterizer(this.surface) { + surface.viewEmbedder = viewEmbedder; + } /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. @@ -30,10 +33,13 @@ class Rasterizer { layerTree.frameSize = frameSize; final SurfaceFrame frame = surface.acquireFrame(layerTree.frameSize); + surface.viewEmbedder.frameSize = layerTree.frameSize; final SkCanvas canvas = frame.skiaCanvas; - final Frame compositorFrame = context.acquireFrame(canvas); + final Frame compositorFrame = context.acquireFrame(canvas, surface.viewEmbedder); compositorFrame.raster(layerTree, ignoreRasterCache: true); + surface.addToScene(); frame.submit(); + surface.viewEmbedder.submitFrame(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/surface.dart b/lib/web_ui/lib/src/engine/compositor/surface.dart index 8e38b678f2fbc..4671a8073df74 100644 --- a/lib/web_ui/lib/src/engine/compositor/surface.dart +++ b/lib/web_ui/lib/src/engine/compositor/surface.dart @@ -34,29 +34,44 @@ class SurfaceFrame { /// created. class Surface { SkSurface _surface; + html.Element htmlElement; + + bool _addedToScene = false; + + /// The default view embedder. Coordinates embedding platform views and + /// overlaying subsequent draw operations on top. + HtmlViewEmbedder viewEmbedder; /// Acquire a frame of the given [size] containing a drawable canvas. /// /// The given [size] is in physical pixels. SurfaceFrame acquireFrame(ui.Size size) { - final SkSurface surface = _acquireRenderSurface(size); + final SkSurface surface = acquireRenderSurface(size); if (surface == null) return null; - SubmitCallback submitCallback = (SurfaceFrame surfaceFrame, SkCanvas canvas) { + SubmitCallback submitCallback = + (SurfaceFrame surfaceFrame, SkCanvas canvas) { _presentSurface(canvas); }; return SurfaceFrame(surface, submitCallback); } - SkSurface _acquireRenderSurface(ui.Size size) { + SkSurface acquireRenderSurface(ui.Size size) { if (!_createOrUpdateSurfaces(size)) { return null; } return _surface; } + void addToScene() { + if (!_addedToScene) { + skiaSceneHost.children.insert(0, htmlElement); + } + _addedToScene = true; + } + bool _createOrUpdateSurfaces(ui.Size size) { if (_surface != null && size == @@ -69,6 +84,9 @@ class Surface { _surface?.dispose(); _surface = null; + htmlElement?.remove(); + htmlElement = null; + _addedToScene = false; if (size.isEmpty) { html.window.console.error('Cannot create surfaces of empty size.'); @@ -86,25 +104,29 @@ class Surface { SkSurface _wrapHtmlCanvas(ui.Size size) { final ui.Size logicalSize = size / ui.window.devicePixelRatio; - final html.CanvasElement htmlCanvas = - html.CanvasElement(width: size.width.ceil(), height: size.height.ceil()) - ..id = 'flt-sk-canvas'; + final html.CanvasElement htmlCanvas = html.CanvasElement( + width: size.width.ceil(), height: size.height.ceil()); htmlCanvas.style ..position = 'absolute' ..width = '${logicalSize.width.ceil()}px' ..height = '${logicalSize.height.ceil()}px'; + final js.JsObject glContext = canvasKit + .callMethod('GetWebGLContext', [htmlCanvas]); + final js.JsObject grContext = + canvasKit.callMethod('MakeGrContext', [glContext]); final js.JsObject skSurface = - canvasKit.callMethod('MakeWebGLCanvasSurface', [ - htmlCanvas, + canvasKit.callMethod('MakeOnScreenGLSurface', [ + grContext, size.width, size.height, ]); + htmlElement = htmlCanvas; + if (skSurface == null) { return null; } else { - domRenderer.renderScene(htmlCanvas); - return SkSurface(skSurface); + return SkSurface(skSurface, glContext); } } @@ -113,6 +135,7 @@ class Surface { return false; } + canvasKit.callMethod('setCurrentContext', [_surface.context]); _surface.getCanvas().flush(); return true; } @@ -121,14 +144,17 @@ class Surface { /// A Dart wrapper around Skia's SkSurface. class SkSurface { final js.JsObject _surface; + final js.JsObject _glContext; - SkSurface(this._surface); + SkSurface(this._surface, this._glContext); SkCanvas getCanvas() { final js.JsObject skCanvas = _surface.callMethod('getCanvas'); return SkCanvas(skCanvas); } + js.JsObject get context => _glContext; + int width() => _surface.callMethod('width'); int height() => _surface.callMethod('height'); diff --git a/lib/web_ui/lib/src/engine/platform_views.dart b/lib/web_ui/lib/src/engine/platform_views.dart index 33980510f3867..1675072eebb7f 100644 --- a/lib/web_ui/lib/src/engine/platform_views.dart +++ b/lib/web_ui/lib/src/engine/platform_views.dart @@ -6,7 +6,7 @@ part of engine; /// A registry for factories that create platform views. class PlatformViewRegistry { - final Map _registeredFactories = + final Map registeredFactories = {}; final Map _createdViews = {}; @@ -16,10 +16,10 @@ class PlatformViewRegistry { /// Register [viewTypeId] as being creating by the given [factory]. bool registerViewFactory(String viewTypeId, PlatformViewFactory factory) { - if (_registeredFactories.containsKey(viewTypeId)) { + if (registeredFactories.containsKey(viewTypeId)) { return false; } - _registeredFactories[viewTypeId] = factory; + registeredFactories[viewTypeId] = factory; return true; } @@ -65,7 +65,7 @@ void _createPlatformView( const MethodCodec codec = StandardMethodCodec(); // TODO(het): Use 'direction', 'width', and 'height'. - if (!platformViewRegistry._registeredFactories.containsKey(viewType)) { + if (!platformViewRegistry.registeredFactories.containsKey(viewType)) { callback(codec.encodeErrorEnvelope( code: 'Unregistered factory', message: "No factory registered for viewtype '$viewType'", @@ -74,7 +74,7 @@ void _createPlatformView( } // TODO(het): Use creation parameters. final html.Element element = - platformViewRegistry._registeredFactories[viewType](id); + platformViewRegistry.registeredFactories[viewType](id); platformViewRegistry._createdViews[id] = element; callback(codec.encodeSuccessEnvelope(null)); diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 96f6409a8d057..69aec0cbeecfe 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -159,7 +159,11 @@ class EngineWindow extends ui.Window { return; case 'flutter/platform_views': - handlePlatformViewCall(data, callback); + if (experimentalUseSkia) { + _rasterizer.viewEmbedder.handlePlatformViewCall(data, callback); + } else { + handlePlatformViewCall(data, callback); + } return; case 'flutter/accessibility':