diff --git a/lib/web_ui/lib/src/engine/color_filter.dart b/lib/web_ui/lib/src/engine/color_filter.dart index 83a7ca27e2cc0..a74145c08bd78 100644 --- a/lib/web_ui/lib/src/engine/color_filter.dart +++ b/lib/web_ui/lib/src/engine/color_filter.dart @@ -135,4 +135,7 @@ class EngineColorFilter implements SceneImageFilter, ui.ColorFilter { return 'ColorFilter.srgbToLinearGamma()'; } } + + @override + Matrix4? get transform => null; } diff --git a/lib/web_ui/lib/src/engine/layers.dart b/lib/web_ui/lib/src/engine/layers.dart index f3658ddee3c4c..1a81d67cb94c2 100644 --- a/lib/web_ui/lib/src/engine/layers.dart +++ b/lib/web_ui/lib/src/engine/layers.dart @@ -5,15 +5,54 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; -import 'package:ui/src/engine/scene_painting.dart'; -import 'package:ui/src/engine/vector_math.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -class EngineRootLayer with PictureEngineLayer {} +class EngineRootLayer with PictureEngineLayer { + @override + final NoopOperation operation = const NoopOperation(); + + @override + EngineRootLayer emptyClone() => EngineRootLayer(); +} + +class NoopOperation implements LayerOperation { + const NoopOperation(); + + @override + PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); + + @override + ui.Rect mapRect(ui.Rect contentRect) => contentRect; + + @override + void pre(SceneCanvas canvas) { + canvas.save(); + } + + @override + void post(SceneCanvas canvas) { + canvas.restore(); + } + + @override + bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'NoopOperation()'; +} class BackdropFilterLayer with PictureEngineLayer - implements ui.BackdropFilterEngineLayer {} + implements ui.BackdropFilterEngineLayer { + BackdropFilterLayer(this.operation); + + @override + final LayerOperation operation; + + @override + BackdropFilterLayer emptyClone() => BackdropFilterLayer(operation); +} class BackdropFilterOperation implements LayerOperation { BackdropFilterOperation(this.filter, this.mode); @@ -24,12 +63,12 @@ class BackdropFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayerWithFilter(contentRect, ui.Paint()..blendMode = mode, filter); + void pre(SceneCanvas canvas) { + canvas.saveLayerWithFilter(ui.Rect.largest, ui.Paint()..blendMode = mode, filter); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -40,11 +79,22 @@ class BackdropFilterOperation implements LayerOperation { // no pictures, so we return true here. @override bool get shouldDrawIfEmpty => true; + + @override + String toString() => 'BackdropFilterOperation(filter: $filter, mode: $mode)'; } class ClipPathLayer with PictureEngineLayer - implements ui.ClipPathEngineLayer {} + implements ui.ClipPathEngineLayer { + ClipPathLayer(this.operation); + + @override + final ClipPathOperation operation; + + @override + ClipPathLayer emptyClone() => ClipPathLayer(operation); +} class ClipPathOperation implements LayerOperation { ClipPathOperation(this.path, this.clip); @@ -55,7 +105,7 @@ class ClipPathOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(path.getBounds()); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -64,7 +114,7 @@ class ClipPathOperation implements LayerOperation { } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -78,11 +128,22 @@ class ClipPathOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'ClipPathOperation(path: $path, clip: $clip)'; } class ClipRectLayer with PictureEngineLayer - implements ui.ClipRectEngineLayer {} + implements ui.ClipRectEngineLayer { + ClipRectLayer(this.operation); + + @override + final ClipRectOperation operation; + + @override + ClipRectLayer emptyClone() => ClipRectLayer(operation); +} class ClipRectOperation implements LayerOperation { const ClipRectOperation(this.rect, this.clip); @@ -93,7 +154,7 @@ class ClipRectOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rect); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.clipRect(rect, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -102,7 +163,7 @@ class ClipRectOperation implements LayerOperation { } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -116,11 +177,22 @@ class ClipRectOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'ClipRectOperation(rect: $rect, clip: $clip)'; } class ClipRRectLayer with PictureEngineLayer - implements ui.ClipRRectEngineLayer {} + implements ui.ClipRRectEngineLayer { + ClipRRectLayer(this.operation); + + @override + final ClipRRectOperation operation; + + @override + ClipRRectLayer emptyClone() => ClipRRectLayer(operation); +} class ClipRRectOperation implements LayerOperation { const ClipRRectOperation(this.rrect, this.clip); @@ -131,7 +203,7 @@ class ClipRRectOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rrect.outerRect); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.clipRRect(rrect, doAntiAlias: clip != ui.Clip.hardEdge); if (clip == ui.Clip.antiAliasWithSaveLayer) { @@ -140,7 +212,7 @@ class ClipRRectOperation implements LayerOperation { } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (clip == ui.Clip.antiAliasWithSaveLayer) { canvas.restore(); } @@ -154,11 +226,22 @@ class ClipRRectOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'ClipRRectOperation(rrect: $rrect, clip: $clip)'; } class ColorFilterLayer with PictureEngineLayer - implements ui.ColorFilterEngineLayer {} + implements ui.ColorFilterEngineLayer { + ColorFilterLayer(this.operation); + + @override + final ColorFilterOperation operation; + + @override + ColorFilterLayer emptyClone() => ColorFilterLayer(operation); +} class ColorFilterOperation implements LayerOperation { ColorFilterOperation(this.filter); @@ -168,12 +251,12 @@ class ColorFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayer(contentRect, ui.Paint()..colorFilter = filter); + void pre(SceneCanvas canvas) { + canvas.saveLayer(ui.Rect.largest, ui.Paint()..colorFilter = filter); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -182,11 +265,22 @@ class ColorFilterOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'ColorFilterOperation(filter: $filter)'; } class ImageFilterLayer with PictureEngineLayer - implements ui.ImageFilterEngineLayer {} + implements ui.ImageFilterEngineLayer { + ImageFilterLayer(this.operation); + + @override + final ImageFilterOperation operation; + + @override + ImageFilterLayer emptyClone() => ImageFilterLayer(operation); +} class ImageFilterOperation implements LayerOperation { ImageFilterOperation(this.filter, this.offset); @@ -197,17 +291,16 @@ class ImageFilterOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => filter.filterBounds(contentRect); @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { if (offset != ui.Offset.zero) { canvas.save(); canvas.translate(offset.dx, offset.dy); } - final ui.Rect adjustedContentRect = filter.filterBounds(contentRect); - canvas.saveLayer(adjustedContentRect, ui.Paint()..imageFilter = filter); + canvas.saveLayer(ui.Rect.largest, ui.Paint()..imageFilter = filter); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { if (offset != ui.Offset.zero) { canvas.restore(); } @@ -216,22 +309,42 @@ class ImageFilterOperation implements LayerOperation { @override PlatformViewStyling createPlatformViewStyling() { + PlatformViewStyling styling = const PlatformViewStyling(); if (offset != ui.Offset.zero) { - return PlatformViewStyling( + styling = PlatformViewStyling( position: PlatformViewPosition.offset(offset) ); - } else { - return const PlatformViewStyling(); } + final Matrix4? transform = filter.transform; + if (transform != null) { + styling = PlatformViewStyling.combine( + styling, + PlatformViewStyling( + position: PlatformViewPosition.transform(transform), + ), + ); + } + return const PlatformViewStyling(); } @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'ImageFilterOperation(filter: $filter)'; } class OffsetLayer with PictureEngineLayer - implements ui.OffsetEngineLayer {} + implements ui.OffsetEngineLayer { + OffsetLayer(this.operation); + + @override + final OffsetOperation operation; + + @override + OffsetLayer emptyClone() => OffsetLayer(operation); +} class OffsetOperation implements LayerOperation { OffsetOperation(this.dx, this.dy); @@ -242,13 +355,13 @@ class OffsetOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(ui.Offset(dx, dy)); @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.translate(dx, dy); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -259,11 +372,22 @@ class OffsetOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'OffsetOperation(dx: $dx, dy: $dy)'; } class OpacityLayer with PictureEngineLayer - implements ui.OpacityEngineLayer {} + implements ui.OpacityEngineLayer { + OpacityLayer(this.operation); + + @override + final OpacityOperation operation; + + @override + OpacityLayer emptyClone() => OpacityLayer(operation); +} class OpacityOperation implements LayerOperation { OpacityOperation(this.alpha, this.offset); @@ -274,20 +398,19 @@ class OpacityOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(offset); @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { + void pre(SceneCanvas canvas) { if (offset != ui.Offset.zero) { canvas.save(); canvas.translate(offset.dx, offset.dy); - cullRect = cullRect.shift(-offset); } canvas.saveLayer( - cullRect, + ui.Rect.largest, ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0) ); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); if (offset != ui.Offset.zero) { canvas.restore(); @@ -302,11 +425,22 @@ class OpacityOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'OpacityOperation(offset: $offset, alpha: $alpha)'; } class TransformLayer with PictureEngineLayer - implements ui.TransformEngineLayer {} + implements ui.TransformEngineLayer { + TransformLayer(this.operation); + + @override + final TransformOperation operation; + + @override + TransformLayer emptyClone() => TransformLayer(operation); +} class TransformOperation implements LayerOperation { TransformOperation(this.transform); @@ -319,13 +453,13 @@ class TransformOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => matrix.transformRect(contentRect); @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { + void pre(SceneCanvas canvas) { canvas.save(); canvas.transform(transform); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.restore(); } @@ -336,11 +470,22 @@ class TransformOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'TransformOperation(matrix: $matrix)'; } class ShaderMaskLayer with PictureEngineLayer - implements ui.ShaderMaskEngineLayer {} + implements ui.ShaderMaskEngineLayer { + ShaderMaskLayer(this.operation); + + @override + final ShaderMaskOperation operation; + + @override + ShaderMaskLayer emptyClone() => ShaderMaskLayer(operation); +} class ShaderMaskOperation implements LayerOperation { ShaderMaskOperation(this.shader, this.maskRect, this.blendMode); @@ -352,15 +497,15 @@ class ShaderMaskOperation implements LayerOperation { ui.Rect mapRect(ui.Rect contentRect) => contentRect; @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { + void pre(SceneCanvas canvas) { canvas.saveLayer( - contentRect, + ui.Rect.largest, ui.Paint(), ); } @override - void post(SceneCanvas canvas, ui.Rect contentRect) { + void post(SceneCanvas canvas) { canvas.save(); canvas.translate(maskRect.left, maskRect.top); canvas.drawRect( @@ -378,6 +523,9 @@ class ShaderMaskOperation implements LayerOperation { @override bool get shouldDrawIfEmpty => false; + + @override + String toString() => 'ShaderMaskOperation(shader: $shader, maskRect: $maskRect, blendMode: $blendMode)'; } class PlatformView { @@ -389,48 +537,60 @@ class PlatformView { final ui.Rect bounds; final PlatformViewStyling styling; -} -sealed class LayerSlice { - void dispose(); + @override + String toString() { + return 'PlatformView(viewId: $viewId, bounds: $bounds, styling: $styling)'; + } } -// A slice that contains one or more platform views to be rendered. -class PlatformViewSlice implements LayerSlice { - PlatformViewSlice(this.views, this.occlusionRect); +class LayerSlice { + LayerSlice(this.picture, this.platformViews); - List views; + // The picture of native flutter content to be rendered + ScenePicture picture; - // A conservative estimate of what area platform views in this slice may cover. - // This is expressed in the coordinate space of the parent. - ui.Rect? occlusionRect; + // Platform views to be placed on top of the flutter content. + final List platformViews; - @override - void dispose() {} + void dispose() { + picture.dispose(); + } } -// A slice that contains flutter content to be rendered int he form of a single -// ScenePicture. -class PictureSlice implements LayerSlice { - PictureSlice(this.picture); +mixin PictureEngineLayer implements ui.EngineLayer { + // Each layer is represented as a series of "slices" which contain flutter content + // with platform views on top. This is ordered from bottommost to topmost. + List slices = []; - ScenePicture picture; + List drawCommands = []; + PlatformViewStyling platformViewStyling = const PlatformViewStyling(); - @override - void dispose() => picture.dispose(); -} + LayerOperation get operation; -mixin PictureEngineLayer implements ui.EngineLayer { - // Each layer is represented as a series of "slices" which contain either - // flutter content or platform views. Slices in this list are ordered from - // bottom to top. - List slices = []; + PictureEngineLayer emptyClone(); @override void dispose() { - for (final LayerSlice slice in slices) { - slice.dispose(); + for (final LayerSlice? slice in slices) { + slice?.dispose(); + } + } + + @override + String toString() { + return 'PictureEngineLayer($operation)'; + } + + bool get isSimple { + if (slices.length > 1) { + return false; + } + final LayerSlice? singleSlice = slices.firstOrNull; + if (singleSlice == null || singleSlice.platformViews.isEmpty) { + return true; } + return false; } } @@ -442,8 +602,8 @@ abstract class LayerOperation { // layer operation. ui.Rect mapRect(ui.Rect contentRect); - void pre(SceneCanvas canvas, ui.Rect contentRect); - void post(SceneCanvas canvas, ui.Rect contentRect); + void pre(SceneCanvas canvas); + void post(SceneCanvas canvas); PlatformViewStyling createPlatformViewStyling(); @@ -453,11 +613,29 @@ abstract class LayerOperation { bool get shouldDrawIfEmpty; } -class PictureDrawCommand { - PictureDrawCommand(this.offset, this.picture); +sealed class LayerDrawCommand { +} + +class PictureDrawCommand extends LayerDrawCommand { + PictureDrawCommand(this.offset, this.picture, this.sliceIndex); - ui.Offset offset; - ui.Picture picture; + final int sliceIndex; + final ui.Offset offset; + final ScenePicture picture; +} + +class PlatformViewDrawCommand extends LayerDrawCommand { + PlatformViewDrawCommand(this.viewId, this.bounds, this.sliceIndex); + + final int sliceIndex; + final int viewId; + final ui.Rect bounds; +} + +class RetainedLayerDrawCommand extends LayerDrawCommand { + RetainedLayerDrawCommand(this.layer); + + final PictureEngineLayer layer; } // Represents how a platform view should be positioned in the scene. @@ -477,6 +655,17 @@ class PlatformViewPosition { bool get isZero => (offset == null) && (transform == null); + ui.Rect mapLocalToGlobal(ui.Rect rect) { + if (offset != null) { + return rect.shift(offset!); + } + if (transform != null) { + return transform!.transformRect(rect); + } + return rect; + } + + // Note that by construction only one of these can be set at any given time, not both. final ui.Offset? offset; final Matrix4? transform; @@ -527,6 +716,17 @@ class PlatformViewPosition { int get hashCode { return Object.hash(offset, transform); } + + @override + String toString() { + if (offset != null) { + return 'PlatformViewPosition(offset: $offset)'; + } + if (transform != null) { + return 'PlatformViewPosition(transform: $transform)'; + } + return 'PlatformViewPosition(zero)'; + } } // Represents the styling to be performed on a platform view when it is @@ -545,6 +745,10 @@ class PlatformViewStyling { final double opacity; final PlatformViewClip clip; + ui.Rect mapLocalToGlobal(ui.Rect rect) { + return position.mapLocalToGlobal(rect.intersect(clip.outerRect)); + } + static PlatformViewStyling combine(PlatformViewStyling outer, PlatformViewStyling inner) { // Attempt to reuse one of the existing immutable objects. if (outer.isDefault) { @@ -575,6 +779,11 @@ class PlatformViewStyling { int get hashCode { return Object.hash(position, opacity, clip); } + + @override + String toString() { + return 'PlatformViewStyling(position: $position, clip: $clip, opacity: $opacity)'; + } } sealed class PlatformViewClip { @@ -642,7 +851,7 @@ class PlatformViewNoClip implements PlatformViewClip { ui.Rect get innerRect => ui.Rect.zero; @override - ui.Rect get outerRect => ui.Rect.zero; + ui.Rect get outerRect => ui.Rect.largest; } class PlatformViewRectClip implements PlatformViewClip { @@ -763,164 +972,145 @@ class PlatformViewPathClip implements PlatformViewClip { ui.Rect get outerRect => path.getBounds(); } +class LayerSliceBuilder { + @visibleForTesting + static (ui.PictureRecorder, SceneCanvas) Function(ui.Rect)? debugRecorderFactory; + + static (ui.PictureRecorder, SceneCanvas) defaultRecorderFactory(ui.Rect rect) { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; + return (recorder, canvas); + } + + void addPicture(ui.Offset offset, ScenePicture picture) { + pictures.add((picture, offset)); + final ui.Rect pictureRect = picture.cullRect.shift(offset); + cullRect = cullRect?.expandToInclude(pictureRect) ?? pictureRect; + } + + (ui.PictureRecorder, SceneCanvas) createRecorder(ui.Rect rect) => + debugRecorderFactory != null ? debugRecorderFactory!(rect) : defaultRecorderFactory(rect); + + LayerSlice buildWithOperation(LayerOperation operation) { + final ui.Rect recorderRect = operation.mapRect(cullRect ?? ui.Rect.zero); + final (recorder, canvas) = createRecorder(recorderRect); + operation.pre(canvas); + for (final (picture, offset) in pictures) { + if (offset != ui.Offset.zero) { + canvas.save(); + canvas.translate(offset.dx, offset.dy); + canvas.drawPicture(picture); + canvas.restore(); + } else { + canvas.drawPicture(picture); + } + } + operation.post(canvas); + final ui.Picture picture = recorder.endRecording(); + return LayerSlice(picture as ScenePicture, platformViews); + } + + final List<(ScenePicture, ui.Offset)> pictures = []; + ui.Rect? cullRect; + final List platformViews = []; +} + class LayerBuilder { factory LayerBuilder.rootLayer() { - return LayerBuilder._(null, EngineRootLayer(), null); + return LayerBuilder._(null, EngineRootLayer()); } factory LayerBuilder.childLayer({ required LayerBuilder parent, required PictureEngineLayer layer, - required LayerOperation operation }) { - return LayerBuilder._(parent, layer, operation); + return LayerBuilder._(parent, layer); } LayerBuilder._( this.parent, - this.layer, - this.operation); - - @visibleForTesting - static (ui.PictureRecorder, SceneCanvas) Function(ui.Rect)? debugRecorderFactory; + this.layer); final LayerBuilder? parent; final PictureEngineLayer layer; - final LayerOperation? operation; - final List pendingPictures = []; - List pendingPlatformViews = []; - ui.Rect? picturesRect; - ui.Rect? platformViewRect; - PlatformViewStyling? _memoizedPlatformViewStyling; + final List sliceBuilders = []; + final List drawCommands = []; + PlatformViewStyling? _memoizedPlatformViewStyling; PlatformViewStyling get platformViewStyling { - return _memoizedPlatformViewStyling ??= operation?.createPlatformViewStyling() ?? const PlatformViewStyling(); + return _memoizedPlatformViewStyling ??= layer.operation.createPlatformViewStyling(); } - (ui.PictureRecorder, SceneCanvas) _createRecorder(ui.Rect rect) { - if (debugRecorderFactory != null) { - return debugRecorderFactory!(rect); + PlatformViewStyling? _memoizedGlobalPlatformViewStyling; + PlatformViewStyling get globalPlatformViewStyling { + if (_memoizedGlobalPlatformViewStyling != null) { + return _memoizedGlobalPlatformViewStyling!; } - final ui.PictureRecorder recorder = ui.PictureRecorder(); - final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; - return (recorder, canvas); + if (parent != null) { + return _memoizedGlobalPlatformViewStyling ??= PlatformViewStyling.combine(parent!.globalPlatformViewStyling, platformViewStyling); + } + return _memoizedGlobalPlatformViewStyling ??= platformViewStyling; } - void flushSlices() { - if (pendingPictures.isNotEmpty || (operation?.shouldDrawIfEmpty ?? false)) { - // Merge the existing draw commands into a single picture and add a slice - // with that picture to the slice list. - final ui.Rect drawnRect = picturesRect ?? ui.Rect.zero; - final ui.Rect rect = operation?.mapRect(drawnRect) ?? drawnRect; - final (ui.PictureRecorder recorder, SceneCanvas canvas) = _createRecorder(rect); - - operation?.pre(canvas, rect); - for (final PictureDrawCommand command in pendingPictures) { - if (command.offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(command.offset.dx, command.offset.dy); - canvas.drawPicture(command.picture); - canvas.restore(); - } else { - canvas.drawPicture(command.picture); - } - } - operation?.post(canvas, rect); - final ui.Picture picture = recorder.endRecording(); - layer.slices.add(PictureSlice(picture as ScenePicture)); + LayerSliceBuilder getOrCreateSliceBuilderAtIndex(int index) { + while (sliceBuilders.length <= index) { + sliceBuilders.add(null); } - - if (pendingPlatformViews.isNotEmpty) { - // Take any pending platform views and lower them into a platform view - // slice. - ui.Rect? occlusionRect = platformViewRect; - if (occlusionRect != null && operation != null) { - occlusionRect = operation!.mapRect(occlusionRect); - } - layer.slices.add(PlatformViewSlice(pendingPlatformViews, occlusionRect)); + final LayerSliceBuilder? existingSliceBuilder = sliceBuilders[index]; + if (existingSliceBuilder != null) { + return existingSliceBuilder; } - - pendingPictures.clear(); - pendingPlatformViews = []; - - // All the pictures and platform views have been lowered into slices. Clear - // our occlusion rectangles. - picturesRect = null; - platformViewRect = null; + final LayerSliceBuilder newSliceBuilder = LayerSliceBuilder(); + sliceBuilders[index] = newSliceBuilder; + return newSliceBuilder; } void addPicture( ui.Offset offset, ui.Picture picture, { - bool isComplexHint = false, - bool willChangeHint = false + required int sliceIndex, }) { - final ui.Rect cullRect = (picture as ScenePicture).cullRect; - final ui.Rect shiftedRect = cullRect.shift(offset); - - final ui.Rect? currentPlatformViewRect = platformViewRect; - if (currentPlatformViewRect != null) { - // Whenever we add a picture to our layer, we try to see if the picture - // will overlap with any platform views that are currently on top of our - // drawing surface. If they don't overlap with the platform views, they - // can be grouped with the existing pending pictures. - if (pendingPictures.isEmpty || currentPlatformViewRect.overlaps(shiftedRect)) { - // If they do overlap with the platform views, however, we should flush - // all the current content into slices and start anew with a fresh - // group of pictures and platform views that will be rendered on top of - // the previous content. Note that we also flush if we have no pending - // pictures to group with. This is the case when platform views are - // the first thing in our stack of objects to composite, and it doesn't - // make sense to try to put a picture slice below the first platform - // view slice, even if the picture doesn't overlap. - flushSlices(); - } - } - pendingPictures.add(PictureDrawCommand(offset, picture)); - picturesRect = picturesRect?.expandToInclude(shiftedRect) ?? shiftedRect; + final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); + sliceBuilder.addPicture(offset, picture as ScenePicture); + drawCommands.add(PictureDrawCommand(offset, picture, sliceIndex)); } void addPlatformView( int viewId, { - ui.Offset offset = ui.Offset.zero, - double width = 0.0, - double height = 0.0 + required ui.Rect bounds, + required int sliceIndex, }) { - final ui.Rect bounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); - platformViewRect = platformViewRect?.expandToInclude(bounds) ?? bounds; - pendingPlatformViews.add(PlatformView(viewId, bounds, platformViewStyling)); + final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); + sliceBuilder.platformViews.add(PlatformView(viewId, bounds, platformViewStyling)); + drawCommands.add(PlatformViewDrawCommand(viewId, bounds, sliceIndex)); } void mergeLayer(PictureEngineLayer layer) { - // When we merge layers, we attempt to merge slices as much as possible as - // well, based on ordering of pictures and platform views and reusing the - // occlusion logic for determining where we can lower each picture. - for (final LayerSlice slice in layer.slices) { - switch (slice) { - case PictureSlice(): - addPicture(ui.Offset.zero, slice.picture); - case PlatformViewSlice(): - final ui.Rect? occlusionRect = slice.occlusionRect; - if (occlusionRect != null) { - platformViewRect = platformViewRect?.expandToInclude(occlusionRect) ?? occlusionRect; - } - for (final PlatformView view in slice.views) { - // Merge the platform view styling of this layer with the nested - // platform views. - final PlatformViewStyling styling = PlatformViewStyling.combine( - platformViewStyling, - view.styling, - ); - pendingPlatformViews.add(PlatformView(view.viewId, view.bounds, styling)); - } + for (int i = 0; i < layer.slices.length; i++) { + final LayerSlice? slice = layer.slices[i]; + if (slice != null) { + final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(i); + sliceBuilder.addPicture(ui.Offset.zero, slice.picture); + sliceBuilder.platformViews.addAll(slice.platformViews.map((PlatformView view) { + return PlatformView(view.viewId, view.bounds, PlatformViewStyling.combine(platformViewStyling, view.styling)); + })); } } + drawCommands.add(RetainedLayerDrawCommand(layer)); } - PictureEngineLayer build() { - // Lower any pending pictures or platform views to their respective slices. - flushSlices(); + PictureEngineLayer sliceUp() { + final List slices = sliceBuilders.map( + (LayerSliceBuilder? builder) => builder?.buildWithOperation(layer.operation) + ).toList(); + layer.slices = slices; return layer; } + + PictureEngineLayer build() { + layer.drawCommands = drawCommands; + layer.platformViewStyling = platformViewStyling; + return sliceUp(); + } } diff --git a/lib/web_ui/lib/src/engine/scene_builder.dart b/lib/web_ui/lib/src/engine/scene_builder.dart index 37d29e66f7fdd..9fe4c17c57711 100644 --- a/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/scene_builder.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' as math; import 'dart:typed_data'; import 'package:ui/src/engine.dart'; @@ -62,17 +63,119 @@ class EngineScene implements ui.Scene { final ui.Rect canvasRect = ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()); final ui.Canvas canvas = ui.Canvas(recorder, canvasRect); - // Only rasterizes the picture slices. - for (final PictureSlice slice in rootLayer.slices.whereType()) { - canvas.drawPicture(slice.picture); + // Only rasterizes the pictures. + for (final LayerSlice? slice in rootLayer.slices) { + if (slice != null) { + canvas.drawPicture(slice.picture); + } } return recorder.endRecording().toImageSync(width, height); } } +sealed class OcclusionMapNode { + bool overlaps(ui.Rect rect); + OcclusionMapNode insert(ui.Rect rect); + ui.Rect get boundingBox; +} + +class OcclusionMapEmpty implements OcclusionMapNode { + @override + ui.Rect get boundingBox => ui.Rect.zero; + + @override + OcclusionMapNode insert(ui.Rect rect) => OcclusionMapLeaf(rect); + + @override + bool overlaps(ui.Rect rect) => false; + +} + +class OcclusionMapLeaf implements OcclusionMapNode { + OcclusionMapLeaf(this.rect); + + final ui.Rect rect; + + @override + ui.Rect get boundingBox => rect; + + @override + OcclusionMapNode insert(ui.Rect other) => OcclusionMapBranch(this, OcclusionMapLeaf(other)); + + @override + bool overlaps(ui.Rect other) => rect.overlaps(other); +} + +class OcclusionMapBranch implements OcclusionMapNode { + OcclusionMapBranch(this.left, this.right) + : boundingBox = left.boundingBox.expandToInclude(right.boundingBox); + + final OcclusionMapNode left; + final OcclusionMapNode right; + + @override + final ui.Rect boundingBox; + + double _areaOfUnion(ui.Rect first, ui.Rect second) { + return (math.max(first.right, second.right) - math.min(first.left, second.left)) + * (math.max(first.bottom, second.bottom) - math.max(first.top, second.top)); + } + + @override + OcclusionMapNode insert(ui.Rect other) { + // Try to create nodes with the smallest possible area + final double leftOtherArea = _areaOfUnion(left.boundingBox, other); + final double rightOtherArea = _areaOfUnion(right.boundingBox, other); + final double leftRightArea = boundingBox.width * boundingBox.height; + if (leftOtherArea < rightOtherArea) { + if (leftOtherArea < leftRightArea) { + return OcclusionMapBranch( + left.insert(other), + right, + ); + } + } else { + if (rightOtherArea < leftRightArea) { + return OcclusionMapBranch( + left, + right.insert(other), + ); + } + } + return OcclusionMapBranch(this, OcclusionMapLeaf(other)); + } + + @override + bool overlaps(ui.Rect rect) { + if (!boundingBox.overlaps(rect)) { + return false; + } + return left.overlaps(rect) || right.overlaps(rect); + } +} + +class OcclusionMap { + OcclusionMapNode root = OcclusionMapEmpty(); + + void addRect(ui.Rect rect) => root = root.insert(rect); + + bool overlaps(ui.Rect rect) => root.overlaps(rect); +} + +class SceneSlice { + final OcclusionMap pictureOcclusionMap = OcclusionMap(); + final OcclusionMap platformViewOcclusionMap = OcclusionMap(); +} + class EngineSceneBuilder implements ui.SceneBuilder { LayerBuilder currentBuilder = LayerBuilder.rootLayer(); + final List sceneSlices = [SceneSlice()]; + + // This represents the simplest case with no platform views, which is a fast path + // that allows us to avoid work tracking the pictures themselves. + bool _isSimple = true; + @override void addPerformanceOverlay(int enabledOptions, ui.Rect bounds) { // We don't plan to implement this on the web. @@ -86,15 +189,54 @@ class EngineSceneBuilder implements ui.SceneBuilder { bool isComplexHint = false, bool willChangeHint = false }) { + final int sliceIndex = _placePicture(offset, picture as ScenePicture, currentBuilder.globalPlatformViewStyling); currentBuilder.addPicture( offset, picture, - isComplexHint: - isComplexHint, - willChangeHint: willChangeHint + sliceIndex: sliceIndex, ); } + // This function determines the lowest scene slice that this picture can be placed + // into and adds it to that slice's occlusion map. + // + // The picture is placed in the last slice where it either intersects with a picture + // in the slice or it intersects with a platform view in the preceding slice. If the + // picture intersects with a platform view in the last slice, a new slice is added at + // the end and the picture goes in there. + int _placePicture(ui.Offset offset, ScenePicture picture, PlatformViewStyling styling) { + if (_isSimple) { + // This is the fast path where there are no platform views. The picture should + // just be placed on the bottom (and only) slice. + return 0; + } + final ui.Rect cullRect = picture.cullRect.shift(offset); + final ui.Rect mappedCullRect = styling.mapLocalToGlobal(cullRect); + int sliceIndex = sceneSlices.length; + while (sliceIndex > 0) { + final SceneSlice sliceBelow = sceneSlices[sliceIndex - 1]; + if (sliceBelow.platformViewOcclusionMap.overlaps(mappedCullRect)) { + break; + } + sliceIndex--; + if (sliceBelow.pictureOcclusionMap.overlaps(mappedCullRect)) { + break; + } + } + if (sliceIndex == 0) { + // Don't bother to populate the lowest occlusion map with pictures, since + // we never hit test against pictures in the bottom slice. + return sliceIndex; + } + if (sliceIndex == sceneSlices.length) { + // Insert a new slice. + sceneSlices.add(SceneSlice()); + } + final SceneSlice slice = sceneSlices[sliceIndex]; + slice.pictureOcclusionMap.addRect(mappedCullRect); + return sliceIndex; + } + @override void addPlatformView( int viewId, { @@ -102,17 +244,103 @@ class EngineSceneBuilder implements ui.SceneBuilder { double width = 0.0, double height = 0.0 }) { + final ui.Rect platformViewRect = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); + final int sliceIndex = _placePlatformView(viewId, platformViewRect, currentBuilder.globalPlatformViewStyling); currentBuilder.addPlatformView( viewId, - offset: offset, - width: width, - height: height + bounds: platformViewRect, + sliceIndex: sliceIndex, ); } + // This function determines the lowest scene slice this platform view can be placed + // into and adds it to that slice's occlusion map. + // + // The platform view is placed into the last slice where it intersects with a picture + // or a platform view. + int _placePlatformView( + int viewId, + ui.Rect rect, + PlatformViewStyling styling, + ) { + // Once we add a platform view, we actually have to do proper occlusion tracking. + _isSimple = false; + + final ui.Rect globalPlatformViewRect = styling.mapLocalToGlobal(rect); + int sliceIndex = sceneSlices.length - 1; + while (sliceIndex > 0) { + final SceneSlice slice = sceneSlices[sliceIndex]; + if (slice.platformViewOcclusionMap.overlaps(globalPlatformViewRect) || + slice.pictureOcclusionMap.overlaps(globalPlatformViewRect)) { + break; + } + sliceIndex--; + } + sliceIndex = 0; + final SceneSlice slice = sceneSlices[sliceIndex]; + slice.platformViewOcclusionMap.addRect(globalPlatformViewRect); + return sliceIndex; + } + @override void addRetained(ui.EngineLayer retainedLayer) { - currentBuilder.mergeLayer(retainedLayer as PictureEngineLayer); + final PictureEngineLayer placedEngineLayer = _placeRetainedLayer(retainedLayer as PictureEngineLayer, currentBuilder.globalPlatformViewStyling); + currentBuilder.mergeLayer(placedEngineLayer); + } + + PictureEngineLayer _placeRetainedLayer(PictureEngineLayer retainedLayer, PlatformViewStyling styling) { + if (_isSimple && retainedLayer.isSimple) { + // There are no platform views, so we don't need to do any occlusion tracking + // and can simply merge the layer. + return retainedLayer; + } + bool needsRebuild = false; + final List revisedDrawCommands = []; + final PlatformViewStyling combinedStyling = PlatformViewStyling.combine(styling, retainedLayer.platformViewStyling); + for (final LayerDrawCommand command in retainedLayer.drawCommands) { + switch (command) { + case PictureDrawCommand(offset: final ui.Offset offset, picture: final ScenePicture picture): + final int sliceIndex = _placePicture(offset, picture, combinedStyling); + if (command.sliceIndex != sliceIndex) { + needsRebuild = true; + } + revisedDrawCommands.add(PictureDrawCommand(offset, picture, sliceIndex)); + case PlatformViewDrawCommand(viewId: final int viewId, bounds: final ui.Rect bounds): + final int sliceIndex = _placePlatformView(viewId, bounds, combinedStyling); + if (command.sliceIndex != sliceIndex) { + needsRebuild = true; + } + revisedDrawCommands.add(PlatformViewDrawCommand(viewId, bounds, sliceIndex)); + case RetainedLayerDrawCommand(layer: final PictureEngineLayer sublayer): + final PictureEngineLayer revisedSublayer = _placeRetainedLayer(sublayer, combinedStyling); + if (sublayer != revisedSublayer) { + needsRebuild = true; + } + revisedDrawCommands.add(RetainedLayerDrawCommand(revisedSublayer)); + } + } + + if (!needsRebuild) { + // No elements changed which slice position they are in, so we can simply + // merge the existing layer down and don't have to redraw individual elements. + return retainedLayer; + } + + // Otherwise, we replace the commands of the layer to create a new one. + currentBuilder = LayerBuilder.childLayer(parent: currentBuilder, layer: retainedLayer.emptyClone()); + for (final LayerDrawCommand command in revisedDrawCommands) { + switch (command) { + case PictureDrawCommand(offset: final ui.Offset offset, picture: final ScenePicture picture): + currentBuilder.addPicture(offset, picture, sliceIndex: command.sliceIndex); + case PlatformViewDrawCommand(viewId: final int viewId, bounds: final ui.Rect bounds): + currentBuilder.addPlatformView(viewId, bounds: bounds, sliceIndex: command.sliceIndex); + case RetainedLayerDrawCommand(layer: final PictureEngineLayer layer): + currentBuilder.mergeLayer(layer); + } + } + final PictureEngineLayer newLayer = currentBuilder.build(); + currentBuilder = currentBuilder.parent!; + return newLayer; } @override @@ -133,30 +361,21 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.BlendMode blendMode = ui.BlendMode.srcOver, ui.BackdropFilterEngineLayer? oldLayer, int? backdropId, - }) => pushLayer( - BackdropFilterLayer(), - BackdropFilterOperation(filter, blendMode), - ); + }) => pushLayer(BackdropFilterLayer(BackdropFilterOperation(filter, blendMode))); @override ui.ClipPathEngineLayer pushClipPath( ui.Path path, { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.ClipPathEngineLayer? oldLayer - }) => pushLayer( - ClipPathLayer(), - ClipPathOperation(path as ScenePath, clipBehavior), - ); + }) => pushLayer(ClipPathLayer(ClipPathOperation(path as ScenePath, clipBehavior))); @override ui.ClipRRectEngineLayer pushClipRRect( ui.RRect rrect, { required ui.Clip clipBehavior, ui.ClipRRectEngineLayer? oldLayer - }) => pushLayer( - ClipRRectLayer(), - ClipRRectOperation(rrect, clipBehavior) - ); + }) => pushLayer(ClipRRectLayer(ClipRRectOperation(rrect, clipBehavior))); @override ui.ClipRectEngineLayer pushClipRect( @@ -164,20 +383,14 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.ClipRectEngineLayer? oldLayer }) { - return pushLayer( - ClipRectLayer(), - ClipRectOperation(rect, clipBehavior) - ); + return pushLayer(ClipRectLayer(ClipRectOperation(rect, clipBehavior))); } @override ui.ColorFilterEngineLayer pushColorFilter( ui.ColorFilter filter, { ui.ColorFilterEngineLayer? oldLayer - }) => pushLayer( - ColorFilterLayer(), - ColorFilterOperation(filter), - ); + }) => pushLayer(ColorFilterLayer(ColorFilterOperation(filter))); @override ui.ImageFilterEngineLayer pushImageFilter( @@ -185,8 +398,7 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.Offset offset = ui.Offset.zero, ui.ImageFilterEngineLayer? oldLayer }) => pushLayer( - ImageFilterLayer(), - ImageFilterOperation(filter as SceneImageFilter, offset), + ImageFilterLayer(ImageFilterOperation(filter as SceneImageFilter, offset)), ); @override @@ -194,19 +406,14 @@ class EngineSceneBuilder implements ui.SceneBuilder { double dx, double dy, { ui.OffsetEngineLayer? oldLayer - }) => pushLayer( - OffsetLayer(), - OffsetOperation(dx, dy) - ); + }) => pushLayer(OffsetLayer(OffsetOperation(dx, dy))); @override ui.OpacityEngineLayer pushOpacity(int alpha, { ui.Offset offset = ui.Offset.zero, ui.OpacityEngineLayer? oldLayer - }) => pushLayer( - OpacityLayer(), - OpacityOperation(alpha, offset), - ); + }) => pushLayer(OpacityLayer(OpacityOperation(alpha, offset))); + @override ui.ShaderMaskEngineLayer pushShaderMask( ui.Shader shader, @@ -215,18 +422,14 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.ShaderMaskEngineLayer? oldLayer, ui.FilterQuality filterQuality = ui.FilterQuality.low }) => pushLayer( - ShaderMaskLayer(), - ShaderMaskOperation(shader, maskRect, blendMode) + ShaderMaskLayer(ShaderMaskOperation(shader, maskRect, blendMode)), ); @override ui.TransformEngineLayer pushTransform( Float64List matrix4, { ui.TransformEngineLayer? oldLayer - }) => pushLayer( - TransformLayer(), - TransformOperation(matrix4), - ); + }) => pushLayer(TransformLayer(TransformOperation(matrix4))); @override void setProperties( @@ -261,11 +464,10 @@ class EngineSceneBuilder implements ui.SceneBuilder { currentBuilder.mergeLayer(layer); } - T pushLayer(T layer, LayerOperation operation) { + T pushLayer(T layer) { currentBuilder = LayerBuilder.childLayer( parent: currentBuilder, layer: layer, - operation: operation ); return layer; } diff --git a/lib/web_ui/lib/src/engine/scene_painting.dart b/lib/web_ui/lib/src/engine/scene_painting.dart index 1ef39d24480cd..4f70633328c67 100644 --- a/lib/web_ui/lib/src/engine/scene_painting.dart +++ b/lib/web_ui/lib/src/engine/scene_painting.dart @@ -4,6 +4,8 @@ import 'package:ui/ui.dart' as ui; +import 'vector_math.dart'; + // These are additional APIs that are not part of the `dart:ui` interface that // are needed internally to properly implement a `SceneBuilder` on top of the // generic Canvas/Picture api. @@ -22,6 +24,10 @@ abstract class SceneImageFilter implements ui.ImageFilter { // gives the maximum draw boundary for a picture with the given input bounds after it // has been processed by the filter. ui.Rect filterBounds(ui.Rect inputBounds); + + // The matrix image filter changes the position of the content, so when positioning + // platform views and calculating occlusion we need to take its transform into account. + Matrix4? get transform; } abstract class ScenePath implements ui.Path { diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index 53504204a4bcb..909c3860419b5 100644 --- a/lib/web_ui/lib/src/engine/scene_view.dart +++ b/lib/web_ui/lib/src/engine/scene_view.dart @@ -95,23 +95,24 @@ class EngineSceneView { flutterView.physicalSize.width, flutterView.physicalSize.height, ); - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; final List picturesToRender = []; final List originalPicturesToRender = []; - for (final LayerSlice slice in slices) { - if (slice is PictureSlice) { - final ui.Rect clippedRect = slice.picture.cullRect.intersect(screenBounds); - if (clippedRect.isEmpty) { - // This picture is completely offscreen, so don't render it at all - continue; - } else if (clippedRect == slice.picture.cullRect) { - // The picture doesn't need to be clipped, just render the original - originalPicturesToRender.add(slice.picture); - picturesToRender.add(slice.picture); - } else { - originalPicturesToRender.add(slice.picture); - picturesToRender.add(pictureRenderer.clipPicture(slice.picture, clippedRect)); - } + for (final LayerSlice? slice in slices) { + if (slice == null) { + continue; + } + final ui.Rect clippedRect = slice.picture.cullRect.intersect(screenBounds); + if (clippedRect.isEmpty) { + // This picture is completely offscreen, so don't render it at all + continue; + } else if (clippedRect == slice.picture.cullRect) { + // The picture doesn't need to be clipped, just render the original + originalPicturesToRender.add(slice.picture); + picturesToRender.add(slice.picture); + } else { + originalPicturesToRender.add(slice.picture); + picturesToRender.add(pictureRenderer.clipPicture(slice.picture, clippedRect)); } } final Map renderMap; @@ -132,58 +133,55 @@ class EngineSceneView { final List reusableContainers = List.from(containers); final List newContainers = []; - for (final LayerSlice slice in slices) { - switch (slice) { - case PictureSlice(): - final DomImageBitmap? bitmap = renderMap[slice.picture]; - if (bitmap == null) { - // We didn't render this slice because no part of it is visible. - continue; - } - PictureSliceContainer? container; - for (int j = 0; j < reusableContainers.length; j++) { - final SliceContainer? candidate = reusableContainers[j]; - if (candidate is PictureSliceContainer) { - container = candidate; - reusableContainers[j] = null; - break; - } + for (final LayerSlice? slice in slices) { + if (slice == null) { + continue; + } + final DomImageBitmap? bitmap = renderMap[slice.picture]; + if (bitmap != null) { + PictureSliceContainer? container; + for (int j = 0; j < reusableContainers.length; j++) { + final SliceContainer? candidate = reusableContainers[j]; + if (candidate is PictureSliceContainer) { + container = candidate; + reusableContainers[j] = null; + break; } + } - final ui.Rect clippedBounds = slice.picture.cullRect.intersect(screenBounds); - if (container != null) { - container.bounds = clippedBounds; - } else { - container = PictureSliceContainer(clippedBounds); - } - container.updateContents(); - container.renderBitmap(bitmap); - newContainers.add(container); - - case PlatformViewSlice(): - for (final PlatformView view in slice.views) { - // TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`, - // instead of using `EnginePlatformDispatcher...implicitView` directly, - // or make the FlutterView "register" like in canvaskit. - // Ensure the platform view contents are injected in the DOM. - EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId); - - // Attempt to reuse a container for the existing view - PlatformViewContainer? container; - for (int j = 0; j < reusableContainers.length; j++) { - final SliceContainer? candidate = reusableContainers[j]; - if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) { - container = candidate; - reusableContainers[j] = null; - break; - } - } - container ??= PlatformViewContainer(view.viewId); - container.bounds = view.bounds; - container.styling = view.styling; - container.updateContents(); - newContainers.add(container); + final ui.Rect clippedBounds = slice.picture.cullRect.intersect(screenBounds); + if (container != null) { + container.bounds = clippedBounds; + } else { + container = PictureSliceContainer(clippedBounds); + } + container.updateContents(); + container.renderBitmap(bitmap); + newContainers.add(container); + } + + for (final PlatformView view in slice.platformViews) { + // TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`, + // instead of using `EnginePlatformDispatcher...implicitView` directly, + // or make the FlutterView "register" like in canvaskit. + // Ensure the platform view contents are injected in the DOM. + EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId); + + // Attempt to reuse a container for the existing view + PlatformViewContainer? container; + for (int j = 0; j < reusableContainers.length; j++) { + final SliceContainer? candidate = reusableContainers[j]; + if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) { + container = candidate; + reusableContainers[j] = null; + break; } + } + container ??= PlatformViewContainer(view.viewId); + container.bounds = view.bounds; + container.styling = view.styling; + container.updateContents(); + newContainers.add(container); } } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart index 7c611d110baef..06b8c47cc34d9 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart @@ -88,6 +88,9 @@ class SkwasmBlurFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.blur($sigmaX, $sigmaY, ${tileModeString(tileMode)})'; + + @override + Matrix4? get transform => null; } class SkwasmDilateFilter extends SkwasmImageFilter { @@ -105,6 +108,9 @@ class SkwasmDilateFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.dilate($radiusX, $radiusY)'; + + @override + Matrix4? get transform => null; } class SkwasmErodeFilter extends SkwasmImageFilter { @@ -122,6 +128,9 @@ class SkwasmErodeFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.erode($radiusX, $radiusY)'; + + @override + Matrix4? get transform => null; } class SkwasmMatrixFilter extends SkwasmImageFilter { @@ -144,6 +153,9 @@ class SkwasmMatrixFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.matrix($matrix4, $filterQuality)'; + + @override + Matrix4? get transform => Matrix4.fromFloat32List(toMatrix32(matrix4)); } class SkwasmColorImageFilter extends SkwasmImageFilter { @@ -162,6 +174,9 @@ class SkwasmColorImageFilter extends SkwasmImageFilter { @override String toString() => filter.toString(); + + @override + Matrix4? get transform => null; } class SkwasmComposedImageFilter extends SkwasmImageFilter { @@ -183,6 +198,16 @@ class SkwasmComposedImageFilter extends SkwasmImageFilter { @override String toString() => 'ImageFilter.compose($outer, $inner)'; + + @override + Matrix4? get transform { + final outerTransform = outer.transform; + final innerTransform = inner.transform; + if (outerTransform != null && innerTransform != null) { + return outerTransform.multiplied(innerTransform); + } + return outerTransform ?? innerTransform; + } } typedef ColorFilterHandleBorrow = void Function(ColorFilterHandle handle); diff --git a/lib/web_ui/test/engine/scene_builder_test.dart b/lib/web_ui/test/engine/scene_builder_test.dart index d8826e8da6033..3d39a22f71342 100644 --- a/lib/web_ui/test/engine/scene_builder_test.dart +++ b/lib/web_ui/test/engine/scene_builder_test.dart @@ -16,7 +16,7 @@ void main() { void testMain() { setUpAll(() { - LayerBuilder.debugRecorderFactory = (ui.Rect rect) { + LayerSliceBuilder.debugRecorderFactory = (ui.Rect rect) { final StubSceneCanvas canvas = StubSceneCanvas(); final StubPictureRecorder recorder = StubPictureRecorder(canvas); return (recorder, canvas); @@ -24,7 +24,7 @@ void testMain() { }); tearDownAll(() { - LayerBuilder.debugRecorderFactory = null; + LayerSliceBuilder.debugRecorderFactory = null; }); group('EngineSceneBuilder', () { @@ -35,23 +35,23 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; expect(slices.length, 1); - expect(slices[0], pictureSliceWithRect(pictureRect)); + expect(slices[0], layerSlice(withPictureRect: pictureRect)); }); test('two pictures', () { final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 400, 400, 400); + const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 300, 400, 400); sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; expect(slices.length, 1); - expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(100, 100, 400, 400))); + expect(slices[0], layerSlice(withPictureRect: const ui.Rect.fromLTRB(100, 100, 400, 400))); }); test('picture + platform view (overlapping)', () { @@ -68,10 +68,11 @@ void testMain() { ); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 2); - expect(slices[0], pictureSliceWithRect(pictureRect)); - expect(slices[1], platformViewSliceWithViews([ + final List slices = scene.rootLayer.slices; + expect(slices.length, 1); + expect(slices[0], layerSlice( + withPictureRect: pictureRect, + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); }); @@ -90,12 +91,12 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; expect(slices.length, 2); - expect(slices[0], platformViewSliceWithViews([ + expect(slices[0], layerSlice(withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); - expect(slices[1], pictureSliceWithRect(pictureRect)); + expect(slices[1], layerSlice(withPictureRect: pictureRect)); }); test('platform view sandwich (overlapping)', () { @@ -114,13 +115,14 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 3); - expect(slices[0], pictureSliceWithRect(pictureRect1)); - expect(slices[1], platformViewSliceWithViews([ + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], layerSlice( + withPictureRect: pictureRect1, + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); - expect(slices[2], pictureSliceWithRect(pictureRect2)); + expect(slices[1], layerSlice(withPictureRect: pictureRect2)); }); test('platform view sandwich (non-overlapping)', () { @@ -139,14 +141,15 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; + final List slices = scene.rootLayer.slices; // The top picture does not overlap with the platform view, so it should // be grouped into the slice below it to reduce the number of canvases we // need. - expect(slices.length, 2); - expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(50, 50, 200, 200))); - expect(slices[1], platformViewSliceWithViews([ + expect(slices.length, 1); + expect(slices[0], layerSlice( + withPictureRect: const ui.Rect.fromLTRB(50, 50, 200, 200), + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling()) ])); }); @@ -169,34 +172,99 @@ void testMain() { sceneBuilder.addPicture(ui.Offset.zero, StubPicture(const ui.Rect.fromLTRB(0, 0, 100, 100))); final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 3); - expect(slices[0], pictureSliceWithRect(pictureRect1)); - expect(slices[1], platformViewSliceWithViews([ + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], layerSlice( + withPictureRect: pictureRect1, + withPlatformViews: [ PlatformView(1, platformViewRect, const PlatformViewStyling(position: PlatformViewPosition.offset(ui.Offset(150, 150)))) ])); - expect(slices[2], pictureSliceWithRect(const ui.Rect.fromLTRB(200, 200, 300, 300))); + expect(slices[1], layerSlice(withPictureRect: const ui.Rect.fromLTRB(200, 200, 300, 300))); + }); + + test('grid view test', () { + // This test case covers a grid of elements, where each element is a platform + // view that has flutter content underneath it and on top of it. + // See a detailed explanation of this use-case in the following flutter issue: + // https://github.com/flutter/flutter/issues/149863 + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const double padding = 10; + const double tileSize = 50; + final List expectedPlatformViews = []; + for (int x = 0; x < 10; x++) { + for (int y = 0; y < 10; y++) { + final ui.Offset offset = ui.Offset( + padding + (tileSize + padding) * x, + padding + (tileSize + padding) * y, + ); + sceneBuilder.pushOffset(offset.dx, offset.dy); + sceneBuilder.addPicture( + ui.Offset.zero, + StubPicture(const ui.Rect.fromLTWH(0, 0, tileSize, tileSize)) + ); + sceneBuilder.addPlatformView( + 1, + offset: const ui.Offset(5, 5), + width: tileSize - 10, + height: tileSize - 10, + ); + sceneBuilder.addPicture( + const ui.Offset(10, 10), + StubPicture(const ui.Rect.fromLTWH(0, 0, tileSize - 20, tileSize - 20)), + ); + sceneBuilder.pop(); + expectedPlatformViews.add(PlatformView( + 1, + const ui.Rect.fromLTRB(5.0, 5.0, tileSize - 5.0, tileSize - 5.0), + PlatformViewStyling(position: PlatformViewPosition.offset(offset)) + )); + } + } + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + + // It is important that the optimizations of the scene builder result in + // there only being two scene slices. + expect(slices.length, 2); + expect(slices[0], layerSlice( + withPictureRect: const ui.Rect.fromLTRB( + padding, + padding, + 10 * (padding + tileSize), + 10 * (padding + tileSize) + ), + withPlatformViews: expectedPlatformViews, + )); + expect(slices[1], layerSlice(withPictureRect: const ui.Rect.fromLTRB( + padding + 10, + padding + 10, + 10 * (padding + tileSize) - 10, + 10 * (padding + tileSize) - 10, + ))); }); }); } -PictureSliceMatcher pictureSliceWithRect(ui.Rect rect) => PictureSliceMatcher(rect); -PlatformViewSliceMatcher platformViewSliceWithViews(List views) - => PlatformViewSliceMatcher(views); +LayerSliceMatcher layerSlice({ + ui.Rect withPictureRect = ui.Rect.zero, + List withPlatformViews = const [], +}) => LayerSliceMatcher(withPictureRect, withPlatformViews); +class LayerSliceMatcher extends Matcher { + LayerSliceMatcher(this.expectedPictureRect, this.expectedPlatformViews); -class PictureSliceMatcher extends Matcher { - PictureSliceMatcher(this.expectedRect); - - final ui.Rect expectedRect; + final ui.Rect expectedPictureRect; + final List expectedPlatformViews; @override Description describe(Description description) { - return description.add(''); + return description.add(''); } @override bool matches(dynamic item, Map matchState) { - if (item is! PictureSlice) { + if (item is! LayerSlice) { return false; } final ScenePicture picture = item.picture; @@ -204,50 +272,28 @@ class PictureSliceMatcher extends Matcher { return false; } - if (picture.cullRect != expectedRect) { + if (picture.cullRect != expectedPictureRect) { return false; } - return true; - } -} - -class PlatformViewSliceMatcher extends Matcher { - PlatformViewSliceMatcher(this.expectedPlatformViews); - - final List expectedPlatformViews; - - @override - Description describe(Description description) { - return description.add(''); - } - - @override - bool matches(dynamic item, Map matchState) { - if (item is! PlatformViewSlice) { + if (item.platformViews.length != expectedPlatformViews.length) { return false; } - if (item.views.length != expectedPlatformViews.length) { - return false; - } - - for (int i = 0; i < item.views.length; i++) { + for (int i = 0; i < item.platformViews.length; i++) { final PlatformView expectedView = expectedPlatformViews[i]; - final PlatformView actualView = item.views[i]; + final PlatformView actualView = item.platformViews[i]; if (expectedView.viewId != actualView.viewId) { - print('viewID mismatch'); return false; } if (expectedView.bounds != actualView.bounds) { - print('bounds mismatch'); return false; } if (expectedView.styling != actualView.styling) { - print('styling mismatch'); return false; } } + return true; } } diff --git a/lib/web_ui/test/engine/scene_builder_utils.dart b/lib/web_ui/test/engine/scene_builder_utils.dart index 033177c8d730b..ec014e70548ff 100644 --- a/lib/web_ui/test/engine/scene_builder_utils.dart +++ b/lib/web_ui/test/engine/scene_builder_utils.dart @@ -36,8 +36,12 @@ class StubPicture implements ScenePicture { class StubCompositePicture extends StubPicture { StubCompositePicture(this.children) : super( children.fold(null, (ui.Rect? previousValue, StubPicture child) { + final ui.Rect childRect = child.cullRect; + if (childRect.isEmpty) { + return previousValue; + } return previousValue?.expandToInclude(child.cullRect) ?? child.cullRect; - })! + }) ?? ui.Rect.zero ); final List children; diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 80093bb0313f9..58ff09de66cdb 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -172,7 +172,7 @@ void testMain() { 120, )); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PictureSlice(picture)); + rootLayer.slices.add(LayerSlice(picture, [])); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); @@ -205,7 +205,7 @@ void testMain() { const ui.Rect.fromLTWH(50, 80, 100, 120), const PlatformViewStyling()); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PlatformViewSlice([platformView], null)); + rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), [platformView])); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); @@ -246,7 +246,7 @@ void testMain() { )); pictures.add(picture); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PictureSlice(picture)); + rootLayer.slices.add(LayerSlice(picture, [])); final EngineScene scene = EngineScene(rootLayer); renderFutures.add(sceneView.renderScene(scene, null)); } @@ -267,7 +267,7 @@ void testMain() { )); final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(PictureSlice(picture)); + rootLayer.slices.add(LayerSlice(picture, [])); final EngineScene scene = EngineScene(rootLayer); await sceneView.renderScene(scene, null); diff --git a/lib/web_ui/test/ui/scene_builder_test.dart b/lib/web_ui/test/ui/scene_builder_test.dart index af17bf98eb3ba..11650de3b435a 100644 --- a/lib/web_ui/test/ui/scene_builder_test.dart +++ b/lib/web_ui/test/ui/scene_builder_test.dart @@ -77,6 +77,23 @@ Future testMain() async { region: region); }); + test('Devtools rendering regression test', () async { + // This is a regression test for https://github.com/flutter/devtools/issues/8401 + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + + sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(12.0, 0.0, 300.0, 27.0)); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawOval( + const ui.Rect.fromLTRB(15.0, 5.0, 64.0, 21.0), + ui.Paint()..color = const ui.Color(0xFF0000FF), + ); + })); + + await renderScene(sceneBuilder.build()); + await matchGoldenFile('scene_builder_oval_clip_rect.png', + region: region); + }); + test('Test clipRRect layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushClipRRect( @@ -224,7 +241,7 @@ Future testMain() async { region: region); }); - test('image filter layer', () async { + test('blur image filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushImageFilter(ui.ImageFilter.blur( sigmaX: 5.0, @@ -240,6 +257,23 @@ Future testMain() async { await matchGoldenFile('scene_builder_image_filter.png', region: region); }); + test('matrix image filter layer', () async { + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder.pushOffset(50.0, 50.0); + + final Matrix4 matrix = Matrix4.rotationZ(math.pi / 18); + final ui.ImageFilter matrixFilter = ui.ImageFilter.matrix(toMatrix64(matrix.storage)); + sceneBuilder.pushImageFilter(matrixFilter); + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawRect( + region, + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); + })); + await renderScene(sceneBuilder.build()); + await matchGoldenFile('scene_builder_matrix_image_filter.png', region: region); + }); + // Regression test for https://github.com/flutter/flutter/issues/154303 test('image filter layer with offset', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();