[web] Refactor renderers to use the same frontend code (#174588)

Refactors the renderer code so both renderers (Skwasm and CanvasKit) use
the same SceneBuilder and platform view embedding code. This change is
discussed in a design doc here:
https://flutter.dev/go/web-renderer-unification

Fixes https://github.com/flutter/flutter/issues/172311
Fixes https://github.com/flutter/flutter/issues/172308
Fixes https://github.com/flutter/flutter/issues/142072

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Harry Terkelsen 2025-08-28 08:27:58 -07:00 committed by GitHub
parent 0628f86a78
commit 111f5a45f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 354 additions and 3723 deletions

View File

@ -74,7 +74,6 @@ export 'engine/layer/layer_scene_builder.dart';
export 'engine/layer/layer_tree.dart';
export 'engine/layer/layer_visitor.dart';
export 'engine/layer/n_way_canvas.dart';
export 'engine/layers.dart';
export 'engine/lazy_path.dart';
export 'engine/mouse/context_menu.dart';
export 'engine/mouse/cursor.dart';
@ -82,11 +81,11 @@ export 'engine/mouse/prevent_default.dart';
export 'engine/navigation/history.dart';
export 'engine/noto_font.dart';
export 'engine/noto_font_encoding.dart';
export 'engine/occlusion_map.dart';
export 'engine/onscreen_logging.dart';
export 'engine/platform_dispatcher.dart';
export 'engine/platform_dispatcher/app_lifecycle_state.dart';
export 'engine/platform_dispatcher/view_focus_binding.dart';
export 'engine/platform_views.dart';
export 'engine/platform_views/content_manager.dart';
export 'engine/platform_views/embedder.dart';
export 'engine/platform_views/message_handler.dart';
@ -99,9 +98,6 @@ export 'engine/profiler.dart';
export 'engine/raw_keyboard.dart';
export 'engine/renderer.dart';
export 'engine/safe_browser_api.dart';
export 'engine/scene_builder.dart';
export 'engine/scene_painting.dart';
export 'engine/scene_view.dart';
export 'engine/semantics/accessibility.dart';
export 'engine/semantics/alert.dart';
export 'engine/semantics/checkable.dart';

View File

@ -312,17 +312,6 @@ CkImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) {
return ckImage;
}
/// Thrown when the web engine fails to decode an image, either due to a
/// network issue, corrupted image contents, or missing codec.
class ImageCodecException implements Exception {
ImageCodecException(this._message);
final String _message;
@override
String toString() => 'ImageCodecException: $_message';
}
const String _kNetworkImageMessage = 'Failed to load network image.';
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after

View File

@ -7,7 +7,7 @@ import 'dart:typed_data';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
class CkPictureRecorder implements LayerPictureRecorder, ScenePictureRecorder {
class CkPictureRecorder implements LayerPictureRecorder {
SkPictureRecorder? _skRecorder;
CkCanvas? _recordingCanvas;

View File

@ -1,52 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ui/ui.dart' as ui;
import '../vector_math.dart';
import 'canvas.dart';
/// A cache of [Picture]s that have already been rasterized.
///
/// In the case of a [Picture] with a lot of complex drawing commands, it can
/// be faster to rasterize that [Picture] into it's own canvas and composite
/// that canvas into the scene rather than replay the drawing commands for that
/// picture into the overall scene.
///
/// This class is responsible for deciding if a [Picture] should be cached and
/// for creating the cached [Picture]s that can be drawn directly into the
/// canvas.
class RasterCache {
/// Make a decision on whether to cache [picture] under transform [matrix].
///
/// This is based on heuristics such as how many times [picture] has been
/// drawn before and the complexity of the drawing commands in [picture].
///
/// We also take into account the current transform [matrix], because, for
/// example, a picture may be rasterized with the identity transform, but
/// when it is used, the transform is a 3x scale. In this case, compositing
/// the rendered picture would result in pixelation. So, we must use both
/// the picture and the transform as a cache key.
///
/// The flag [isComplex] is a hint to the raster cache that this picture
/// contains complex drawing commands, and as such should be more strongly
/// considered for caching.
///
/// The flag [willChange] is a hint to the raster cache that this picture
/// will change, and so should be less likely to be cached.
void prepare(ui.Picture picture, Matrix4 matrix, bool isComplex, bool willChange) {}
/// Gets a raster cache result for the [picture] at transform [matrix].
RasterCacheResult get(ui.Picture picture, Matrix4 matrix) => RasterCacheResult();
}
/// A cache entry for a given picture and matrix.
class RasterCacheResult {
/// Whether or not this result represents a rasterized picture that can be
/// drawn into the scene.
bool get isValid => false;
/// Draws the rasterized picture into the [canvas].
void draw(CkCanvas canvas) {}
}

View File

@ -428,71 +428,6 @@ class CanvasKitRenderer extends Renderer {
ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style) =>
isExperimentalWebParagraph ? WebParagraphBuilder(style) : CkParagraphBuilder(style);
// TODO(harryterkelsen): Merge this logic with the async logic in
// [EngineScene], https://github.com/flutter/flutter/issues/142072.
@override
Future<void> renderScene(ui.Scene scene, EngineFlutterView view) async {
assert(
rasterizers.containsKey(view.viewId),
"Unable to render to a view which hasn't been registered",
);
final ViewRasterizer rasterizer = rasterizers[view.viewId]!;
final RenderQueue renderQueue = rasterizer.queue;
final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled
? FrameTimingRecorder()
: null;
if (renderQueue.current != null) {
// If a scene is already queued up, drop it and queue this one up instead
// so that the scene view always displays the most recently requested scene.
renderQueue.next?.completer.complete();
final Completer<void> completer = Completer<void>();
renderQueue.next = (scene: scene, completer: completer, recorder: recorder);
return completer.future;
}
final Completer<void> completer = Completer<void>();
renderQueue.current = (scene: scene, completer: completer, recorder: recorder);
unawaited(_kickRenderLoop(rasterizer));
return completer.future;
}
Future<void> _kickRenderLoop(ViewRasterizer rasterizer) async {
final RenderQueue renderQueue = rasterizer.queue;
final RenderRequest current = renderQueue.current!;
try {
await _renderScene(current.scene, rasterizer, current.recorder);
current.completer.complete();
} catch (error, stackTrace) {
current.completer.completeError(error, stackTrace);
}
renderQueue.current = renderQueue.next;
renderQueue.next = null;
if (renderQueue.current == null) {
return;
} else {
return _kickRenderLoop(rasterizer);
}
}
Future<void> _renderScene(
ui.Scene scene,
ViewRasterizer rasterizer,
FrameTimingRecorder? recorder,
) async {
// "Build finish" and "raster start" happen back-to-back because we
// render on the same thread, so there's no overhead from hopping to
// another thread.
//
// CanvasKit works differently from the HTML renderer in that in HTML
// we update the DOM in SceneBuilder.build, which is these function calls
// here are CanvasKit-only.
recorder?.recordBuildFinish();
recorder?.recordRasterStart();
await rasterizer.draw((scene as LayerScene).layerTree, null);
recorder?.recordRasterFinish();
recorder?.submitTimings();
}
@override
void clearFragmentProgramCache() {
_programs.clear();
@ -535,5 +470,14 @@ class CanvasKitRenderer extends Renderer {
);
@override
void dumpDebugInfo() {}
void dumpDebugInfo() {
int i = 0;
for (final viewRasterizer in rasterizers.values) {
final Map<String, dynamic>? debugJson = viewRasterizer.dumpDebugInfo();
if (debugJson != null) {
downloadDebugInfo('flutter-scene$i', debugJson);
i++;
}
}
}
}

View File

@ -162,9 +162,8 @@ class FrameService {
// In Flutter terminology "building a frame" consists of "beginning
// frame" and "drawing frame".
//
// We do not call `recordBuildFinish` from here because
// part of the rasterization process, particularly in the HTML
// renderer, takes place in the `SceneBuilder.build()`.
// We do not call `recordBuildFinish` from here because part of the
// rasterization process takes place in `SceneBuilder.build()`.
FrameTimingRecorder.recordCurrentFrameBuildStart();
// We have to convert high-resolution time to `int` so we can construct

View File

@ -267,3 +267,14 @@ ui.Image scaleImageIfNeeded(
image.dispose();
return finalImage;
}
/// Thrown when the web engine fails to decode an image, either due to a
/// network issue, corrupted image contents, or missing codec.
class ImageCodecException implements Exception {
ImageCodecException(this._message);
final String _message;
@override
String toString() => 'ImageCodecException: $_message';
}

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ import 'dart:typed_data';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
abstract class DisposablePath implements ScenePath, LayerPath {
abstract class DisposablePath implements LayerPath {
@override
DisposablePathMetrics computeMetrics({bool forceClosed = false});
@ -355,7 +355,7 @@ abstract class DisposablePathConstructors {
);
}
class LazyPath implements ScenePath, LayerPath, Collectable {
class LazyPath implements LayerPath, Collectable {
factory LazyPath(DisposablePathConstructors constructors) =>
LazyPath._(constructors, ui.PathFillType.nonZero, () => constructors.createNew());
LazyPath._(this.constructors, this._fillType, this.initializer) : _commands = [];

View File

@ -0,0 +1,89 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:ui/ui.dart' as ui;
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.min(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);
}

View File

@ -1,8 +0,0 @@
// 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.
// TODO(yjbanov): The code in this file was temporarily moved to lib/web_ui/lib/ui.dart
// during the NNBD migration so that `dart:ui` does not have to export
// `dart:_engine`. NNBD does not allow exported non-migrated libraries
// from migrated libraries.

View File

@ -275,7 +275,53 @@ abstract class Renderer {
/// Map from view id to the associated [ViewRasterizer] for that view.
final Map<int, ViewRasterizer> rasterizers = <int, ViewRasterizer>{};
Future<void> renderScene(ui.Scene scene, EngineFlutterView view);
Future<void> renderScene(ui.Scene scene, EngineFlutterView view) async {
assert(
rasterizers.containsKey(view.viewId),
"Unable to render to a view which hasn't been registered",
);
final ViewRasterizer rasterizer = rasterizers[view.viewId]!;
final RenderQueue renderQueue = rasterizer.queue;
final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled
? FrameTimingRecorder()
: null;
if (renderQueue.current != null) {
// If a scene is already queued up, drop it and queue this one up instead
// so that the scene view always displays the most recently requested scene.
renderQueue.next?.completer.complete();
final Completer<void> completer = Completer<void>();
renderQueue.next = (scene: scene, completer: completer, recorder: recorder);
return completer.future;
}
final Completer<void> completer = Completer<void>();
renderQueue.current = (scene: scene, completer: completer, recorder: recorder);
unawaited(_kickRenderLoop(rasterizer));
return completer.future;
}
Future<void> _kickRenderLoop(ViewRasterizer rasterizer) async {
final RenderQueue renderQueue = rasterizer.queue;
while (renderQueue.current != null) {
final RenderRequest current = renderQueue.current!;
try {
await _renderScene(current.scene, rasterizer, current.recorder);
current.completer.complete();
} catch (error, stackTrace) {
current.completer.completeError(error, stackTrace);
}
renderQueue.current = renderQueue.next;
renderQueue.next = null;
}
}
Future<void> _renderScene(
ui.Scene scene,
ViewRasterizer rasterizer,
FrameTimingRecorder? recorder,
) async {
await rasterizer.draw((scene as LayerScene).layerTree, recorder);
recorder?.submitTimings();
}
void dumpDebugInfo();

View File

@ -1,489 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
// This file implements a SceneBuilder and Scene that works with any renderer
// implementation that provides:
// * A `ui.Canvas` that conforms to `SceneCanvas`
// * A `ui.Picture` that conforms to `ScenePicture`
// * A `ui.ImageFilter` that conforms to `SceneImageFilter`
//
// These contain a few augmentations to the normal `dart:ui` API that provide
// additional sizing information that the scene builder uses to determine how
// these object might occlude one another.
class EngineScene implements ui.Scene {
EngineScene(this.rootLayer);
final EngineRootLayer rootLayer;
// We keep a refcount here because this can be asynchronously rendered, so we
// don't necessarily want to dispose immediately when the user calls dispose.
// Instead, we need to stay alive until we're done rendering.
int _refCount = 1;
void beginRender() {
assert(_refCount > 0);
_refCount++;
}
void endRender() {
_refCount--;
_disposeIfNeeded();
}
@override
void dispose() {
_refCount--;
_disposeIfNeeded();
}
void _disposeIfNeeded() {
assert(_refCount >= 0);
if (_refCount == 0) {
rootLayer.dispose();
}
}
@override
Future<ui.Image> toImage(int width, int height) async {
return toImageSync(width, height);
}
@override
ui.Image toImageSync(int width, int height) {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Rect canvasRect = ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble());
final ui.Canvas canvas = ui.Canvas(recorder, canvasRect);
// Only rasterizes the pictures.
for (final LayerSlice? slice in rootLayer.slices) {
if (slice != null) {
canvas.drawPicture(slice.picture);
}
}
return recorder.endRecording().toImageSync(width, height);
}
Map<String, Object> get debugJsonDescription {
return {'rootLayer': rootLayer.debugJsonDescription};
}
}
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<SceneSlice> sceneSlices = <SceneSlice>[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.
throw UnimplementedError();
}
@override
void addPicture(
ui.Offset offset,
ui.Picture picture, {
bool isComplexHint = false,
bool willChangeHint = false,
}) {
final int sliceIndex = _placePicture(
offset,
picture as ScenePicture,
currentBuilder.globalPlatformViewStyling,
);
currentBuilder.addPicture(offset, picture, 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, {
ui.Offset offset = ui.Offset.zero,
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, 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--;
}
final SceneSlice slice = sceneSlices[sliceIndex];
slice.platformViewOcclusionMap.addRect(globalPlatformViewRect);
return sliceIndex;
}
@override
void addRetained(ui.EngineLayer retainedLayer) {
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<LayerDrawCommand> 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
void addTexture(
int textureId, {
ui.Offset offset = ui.Offset.zero,
double width = 0.0,
double height = 0.0,
bool freeze = false,
ui.FilterQuality filterQuality = ui.FilterQuality.low,
}) {
// addTexture is not implemented on web.
}
@override
ui.BackdropFilterEngineLayer pushBackdropFilter(
ui.ImageFilter filter, {
ui.BlendMode blendMode = ui.BlendMode.srcOver,
ui.BackdropFilterEngineLayer? oldLayer,
int? backdropId,
}) => pushLayer<BackdropFilterLayer>(
BackdropFilterLayer(BackdropFilterOperation(filter, blendMode)),
);
@override
ui.ClipPathEngineLayer pushClipPath(
ui.Path path, {
ui.Clip clipBehavior = ui.Clip.antiAlias,
ui.ClipPathEngineLayer? oldLayer,
}) => pushLayer<ClipPathLayer>(ClipPathLayer(ClipPathOperation(path as ScenePath, clipBehavior)));
@override
ui.ClipRRectEngineLayer pushClipRRect(
ui.RRect rrect, {
required ui.Clip clipBehavior,
ui.ClipRRectEngineLayer? oldLayer,
}) => pushLayer<ClipRRectLayer>(ClipRRectLayer(ClipRRectOperation(rrect, clipBehavior)));
@override
ui.ClipRSuperellipseEngineLayer pushClipRSuperellipse(
ui.RSuperellipse rsuperellipse, {
required ui.Clip clipBehavior,
ui.ClipRSuperellipseEngineLayer? oldLayer,
}) {
return pushLayer<ClipRSuperellipseLayer>(
ClipRSuperellipseLayer(ClipRSuperellipseOperation(rsuperellipse, clipBehavior)),
);
}
@override
ui.ClipRectEngineLayer pushClipRect(
ui.Rect rect, {
ui.Clip clipBehavior = ui.Clip.antiAlias,
ui.ClipRectEngineLayer? oldLayer,
}) {
return pushLayer<ClipRectLayer>(ClipRectLayer(ClipRectOperation(rect, clipBehavior)));
}
@override
ui.ColorFilterEngineLayer pushColorFilter(
ui.ColorFilter filter, {
ui.ColorFilterEngineLayer? oldLayer,
}) => pushLayer<ColorFilterLayer>(ColorFilterLayer(ColorFilterOperation(filter)));
@override
ui.ImageFilterEngineLayer pushImageFilter(
ui.ImageFilter filter, {
ui.Offset offset = ui.Offset.zero,
ui.ImageFilterEngineLayer? oldLayer,
}) => pushLayer<ImageFilterLayer>(
ImageFilterLayer(ImageFilterOperation(filter as SceneImageFilter, offset)),
);
@override
ui.OffsetEngineLayer pushOffset(double dx, double dy, {ui.OffsetEngineLayer? oldLayer}) =>
pushLayer<OffsetLayer>(OffsetLayer(OffsetOperation(dx, dy)));
@override
ui.OpacityEngineLayer pushOpacity(
int alpha, {
ui.Offset offset = ui.Offset.zero,
ui.OpacityEngineLayer? oldLayer,
}) => pushLayer<OpacityLayer>(OpacityLayer(OpacityOperation(alpha, offset)));
@override
ui.ShaderMaskEngineLayer pushShaderMask(
ui.Shader shader,
ui.Rect maskRect,
ui.BlendMode blendMode, {
ui.ShaderMaskEngineLayer? oldLayer,
ui.FilterQuality filterQuality = ui.FilterQuality.low,
}) =>
pushLayer<ShaderMaskLayer>(ShaderMaskLayer(ShaderMaskOperation(shader, maskRect, blendMode)));
@override
ui.TransformEngineLayer pushTransform(Float64List matrix4, {ui.TransformEngineLayer? oldLayer}) =>
pushLayer<TransformLayer>(TransformLayer(TransformOperation(matrix4)));
@override
void setProperties(
double width,
double height,
double insetTop,
double insetRight,
double insetBottom,
double insetLeft,
bool focusable,
) {
// Not implemented on web
}
@override
ui.Scene build() {
while (currentBuilder.parent != null) {
pop();
}
final PictureEngineLayer rootLayer = currentBuilder.build();
return EngineScene(rootLayer as EngineRootLayer);
}
@override
void pop() {
final LayerBuilder? parentBuilder = currentBuilder.parent;
if (parentBuilder == null) {
// Root layer. Nothing to pop.
return;
}
final PictureEngineLayer layer = currentBuilder.build();
currentBuilder = parentBuilder;
currentBuilder.mergeLayer(layer);
}
T pushLayer<T extends PictureEngineLayer>(T layer) {
currentBuilder = LayerBuilder.childLayer(parent: currentBuilder, layer: layer);
return layer;
}
}

View File

@ -1,45 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package: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.
abstract class SceneCanvas implements ui.Canvas {
// This is the same as a normal `saveLayer` call, but we can pass a backdrop image filter.
void saveLayerWithFilter(ui.Rect? bounds, ui.Paint paint, ui.ImageFilter backdropFilter);
}
abstract class ScenePicture implements ui.Picture {
// This is a conservative bounding box of all the drawing primitives in this picture.
ui.Rect get cullRect;
}
abstract class ScenePictureRecorder implements ui.PictureRecorder {
/// Whether this reference to the underlying picture recorder is [dispose]d.
///
/// This only returns a valid value if asserts are enabled, and must not be
/// used otherwise.
bool get debugDisposed;
}
abstract class SceneImageFilter implements ui.ImageFilter {
// Since some image filters affect the actual drawing bounds of a given picture, this
// 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 {
// In order to properly clip platform views with paths, we need to be able to get a
// string representation of them.
String toSvgString();
}

View File

@ -1,405 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
const String kCanvasContainerTag = 'flt-canvas-container';
typedef RenderResult = ({
List<DomImageBitmap> imageBitmaps,
int rasterStartMicros,
int rasterEndMicros,
});
// This is an interface that renders a `ScenePicture` as a `DomImageBitmap`.
// It is optionally asynchronous. It is required for the `EngineSceneView` to
// composite pictures into the canvases in the DOM tree it builds.
abstract class PictureRenderer {
FutureOr<RenderResult> renderPictures(List<ScenePicture> picture, int width, int height);
}
class _SceneRender {
_SceneRender(this.scene, this._completer, {this.recorder}) {
scene.beginRender();
}
final EngineScene scene;
final Completer<void> _completer;
final FrameTimingRecorder? recorder;
void done() {
scene.endRender();
_completer.complete();
}
}
// This class builds a DOM tree that composites an `EngineScene`.
class EngineSceneView {
factory EngineSceneView(PictureRenderer pictureRenderer, EngineFlutterView flutterView) {
final DomElement sceneElement = createDomElement('flt-scene');
return EngineSceneView._(pictureRenderer, flutterView, sceneElement);
}
EngineSceneView._(this.pictureRenderer, this.flutterView, this.sceneElement);
final PictureRenderer pictureRenderer;
final DomElement sceneElement;
final EngineFlutterView flutterView;
List<SliceContainer> containers = <SliceContainer>[];
_SceneRender? _currentRender;
_SceneRender? _nextRender;
// Only populated in debug mode.
_SceneRender? _previousRender;
Future<void> renderScene(EngineScene scene, FrameTimingRecorder? recorder) {
if (_currentRender != null) {
// If a scene is already queued up, drop it and queue this one up instead
// so that the scene view always displays the most recently requested scene.
_nextRender?.done();
final Completer<void> completer = Completer<void>();
_nextRender = _SceneRender(scene, completer, recorder: recorder);
return completer.future;
}
final Completer<void> completer = Completer<void>();
_currentRender = _SceneRender(scene, completer, recorder: recorder);
_kickRenderLoop();
return completer.future;
}
Future<void> _kickRenderLoop() async {
final _SceneRender current = _currentRender!;
await _renderScene(current.scene, current.recorder);
_renderComplete(current);
_currentRender = _nextRender;
_nextRender = null;
if (_currentRender == null) {
return;
} else {
return _kickRenderLoop();
}
}
Future<void> _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async {
final ui.Rect screenBounds = ui.Rect.fromLTWH(
0,
0,
flutterView.physicalSize.width,
flutterView.physicalSize.height,
);
final List<LayerSlice?> slices = scene.rootLayer.slices;
final List<ScenePicture> picturesToRender = <ScenePicture>[];
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 {
picturesToRender.add(slice.picture);
}
}
final Map<ScenePicture, DomImageBitmap> renderMap;
if (picturesToRender.isNotEmpty) {
final RenderResult renderResult = await pictureRenderer.renderPictures(
picturesToRender,
screenBounds.width.ceil(),
screenBounds.height.ceil(),
);
renderMap = <ScenePicture, DomImageBitmap>{
for (int i = 0; i < picturesToRender.length; i++)
picturesToRender[i]: renderResult.imageBitmaps[i],
};
recorder?.recordRasterStart(renderResult.rasterStartMicros);
recorder?.recordRasterFinish(renderResult.rasterEndMicros);
} else {
renderMap = <ScenePicture, DomImageBitmap>{};
recorder?.recordRasterStart();
recorder?.recordRasterFinish();
}
recorder?.submitTimings();
final List<SliceContainer?> reusableContainers = List<SliceContainer?>.from(containers);
final List<SliceContainer> newContainers = <SliceContainer>[];
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;
}
}
if (container != null) {
container.bounds = screenBounds;
} else {
container = PictureSliceContainer(screenBounds);
}
container.updateContents();
container.renderBitmap(bitmap);
newContainers.add(container);
}
for (final PlatformView view in slice.platformViews) {
// Ensure the contents of the platform view are injected into the DOM.
flutterView.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);
}
}
for (final SliceContainer? container in reusableContainers) {
if (container != null) {
sceneElement.removeChild(container.container);
}
}
containers = newContainers;
DomElement? currentElement = sceneElement.firstElementChild;
for (final SliceContainer container in containers) {
if (currentElement == null) {
sceneElement.appendChild(container.container);
} else if (currentElement == container.container) {
currentElement = currentElement.nextElementSibling;
} else {
sceneElement.insertBefore(container.container, currentElement);
}
}
}
void _renderComplete(_SceneRender render) {
render.done();
if (kDebugMode) {
_previousRender = render;
}
}
Map<String, dynamic>? dumpDebugInfo() {
if (kDebugMode && _previousRender != null) {
return _previousRender!.scene.debugJsonDescription;
}
return null;
}
}
sealed class SliceContainer {
DomElement get container;
void updateContents();
}
final class PictureSliceContainer extends SliceContainer {
factory PictureSliceContainer(ui.Rect bounds) {
final DomElement container = domDocument.createElement(kCanvasContainerTag);
final DomHTMLCanvasElement canvas = createDomCanvasElement(
width: bounds.width.toInt(),
height: bounds.height.toInt(),
);
container.appendChild(canvas);
return PictureSliceContainer._(bounds, container, canvas);
}
PictureSliceContainer._(this._bounds, this.container, this.canvas);
ui.Rect _bounds;
bool _dirty = true;
ui.Rect get bounds => _bounds;
set bounds(ui.Rect bounds) {
if (_bounds != bounds) {
_bounds = bounds;
_dirty = true;
}
}
@override
void updateContents() {
if (_dirty) {
_dirty = false;
final ui.Rect roundedOutBounds = ui.Rect.fromLTRB(
bounds.left.floorToDouble(),
bounds.top.floorToDouble(),
bounds.right.ceilToDouble(),
bounds.bottom.ceilToDouble(),
);
final DomCSSStyleDeclaration style = canvas.style;
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
final double logicalWidth = roundedOutBounds.width / devicePixelRatio;
final double logicalHeight = roundedOutBounds.height / devicePixelRatio;
final double logicalLeft = roundedOutBounds.left / devicePixelRatio;
final double logicalTop = roundedOutBounds.top / devicePixelRatio;
style.width = '${logicalWidth}px';
style.height = '${logicalHeight}px';
style.position = 'absolute';
style.left = '${logicalLeft}px';
style.top = '${logicalTop}px';
canvas.width = roundedOutBounds.width.ceilToDouble();
canvas.height = roundedOutBounds.height.ceilToDouble();
}
}
void renderBitmap(DomImageBitmap bitmap) {
final DomImageBitmapRenderingContext ctx = canvas.contextBitmapRenderer;
ctx.transferFromImageBitmap(bitmap);
}
@override
final DomElement container;
final DomHTMLCanvasElement canvas;
}
final class PlatformViewContainer extends SliceContainer {
PlatformViewContainer(this.viewId)
: container = createDomElement('flt-clip'),
slot = createPlatformViewSlot(viewId) {
container.appendChild(slot);
}
final int viewId;
PlatformViewStyling? _styling;
ui.Rect? _bounds;
bool _dirty = false;
ui.Path? _clipPath;
String? _clipPathString;
@override
final DomElement container;
final DomElement slot;
set styling(PlatformViewStyling styling) {
if (_styling != styling) {
_styling = styling;
_dirty = true;
}
}
set bounds(ui.Rect bounds) {
if (_bounds != bounds) {
_bounds = bounds;
_dirty = true;
}
}
set clipPath(ScenePath? path) {
if (_clipPath == path) {
return;
}
_clipPath = path;
_clipPathString = path?.toSvgString();
}
String cssStringForClip(PlatformViewClip clip, double devicePixelRatio) {
switch (clip) {
case PlatformViewNoClip():
clipPath = null;
return '';
case PlatformViewRectClip():
clipPath = null;
final double top = clip.rect.top / devicePixelRatio;
final double right = clip.rect.right / devicePixelRatio;
final double bottom = clip.rect.bottom / devicePixelRatio;
final double left = clip.rect.left / devicePixelRatio;
return 'rect(${top}px ${right}px ${bottom}px ${left}px)';
case PlatformViewRRectClip():
clipPath = null;
final double top = clip.rrect.top / devicePixelRatio;
final double right = clip.rrect.right / devicePixelRatio;
final double bottom = clip.rrect.bottom / devicePixelRatio;
final double left = clip.rrect.left / devicePixelRatio;
final double tlRadiusX = clip.rrect.tlRadiusX / devicePixelRatio;
final double tlRadiusY = clip.rrect.tlRadiusY / devicePixelRatio;
final double trRadiusX = clip.rrect.trRadiusX / devicePixelRatio;
final double trRadiusY = clip.rrect.trRadiusY / devicePixelRatio;
final double brRadiusX = clip.rrect.brRadiusX / devicePixelRatio;
final double brRadiusY = clip.rrect.brRadiusY / devicePixelRatio;
final double blRadiusX = clip.rrect.blRadiusX / devicePixelRatio;
final double blRadiusY = clip.rrect.blRadiusY / devicePixelRatio;
return 'rect(${top}px ${right}px ${bottom}px ${left}px round ${tlRadiusX}px ${trRadiusX}px ${brRadiusX}px ${blRadiusX}px / ${tlRadiusY}px ${trRadiusY}px ${brRadiusY}px ${blRadiusY}px)';
case PlatformViewPathClip():
ScenePath path = clip.path;
if (devicePixelRatio != 1.0) {
final dprTransform = Matrix4.identity()..scale(1 / devicePixelRatio);
path = path.transform(dprTransform.toFloat64()) as ScenePath;
}
clipPath = path;
return "path('$_clipPathString')";
}
}
@override
void updateContents() {
assert(_styling != null);
assert(_bounds != null);
if (_dirty) {
final DomCSSStyleDeclaration style = slot.style;
style.position = 'absolute';
style.width = '${_bounds!.width}px';
style.height = '${_bounds!.height}px';
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
final PlatformViewPosition position = _styling!.position;
final Matrix4 transform;
if (position.transform != null) {
transform = position.transform!.clone()..translate(_bounds!.left, _bounds!.top);
} else {
final ui.Offset offset = position.offset ?? ui.Offset.zero;
transform = Matrix4.translationValues(
_bounds!.left + offset.dx,
_bounds!.top + offset.dy,
0,
);
}
final double inverseScale = 1.0 / devicePixelRatio;
final Matrix4 scaleMatrix = Matrix4.diagonal3Values(inverseScale, inverseScale, 1);
scaleMatrix.multiply(transform);
style.transform = float64ListToCssTransform(scaleMatrix.storage);
style.transformOrigin = '0 0 0';
style.opacity = _styling!.opacity != 1.0 ? '${_styling!.opacity}' : '';
final DomCSSStyleDeclaration containerStyle = container.style;
containerStyle.position = 'absolute';
containerStyle.width = '100%';
containerStyle.height = '100%';
final String clipPathString = cssStringForClip(_styling!.clip, devicePixelRatio);
containerStyle.clipPath = clipPathString;
_dirty = false;
}
}
}

View File

@ -14,6 +14,7 @@ export 'skwasm_impl/filters.dart';
export 'skwasm_impl/font_collection.dart';
export 'skwasm_impl/image.dart';
export 'skwasm_impl/memory.dart';
export 'skwasm_impl/offscreen_canvas_rasterizer.dart';
export 'skwasm_impl/paint.dart';
export 'skwasm_impl/paragraph.dart';
export 'skwasm_impl/path.dart';

View File

@ -9,7 +9,7 @@ import 'package:ui/src/engine.dart';
import 'package:ui/src/engine/skwasm/skwasm_impl.dart';
import 'package:ui/ui.dart' as ui;
class SkwasmCanvas implements SceneCanvas {
class SkwasmCanvas implements LayerCanvas {
factory SkwasmCanvas(SkwasmPictureRecorder recorder, ui.Rect cullRect) => SkwasmCanvas.fromHandle(
withStackScope(
(StackScope s) =>
@ -412,4 +412,16 @@ class SkwasmCanvas implements SceneCanvas {
return scope.convertMatrix44FromNative(outMatrix);
});
}
@override
void clear(ui.Color color) {
canvasClear(_handle, color.value);
}
@override
bool quickReject(ui.Rect rect) {
return withStackScope((StackScope s) {
return canvasQuickReject(_handle, s.convertRectToNative(rect));
});
}
}

View File

@ -11,7 +11,7 @@ import 'package:ui/ui.dart' as ui;
typedef ImageFilterHandleBorrow<T> = T Function(ImageFilterHandle handle);
abstract class SkwasmImageFilter implements SceneImageFilter {
abstract class SkwasmImageFilter implements LayerImageFilter {
const SkwasmImageFilter();
factory SkwasmImageFilter.blur({

View File

@ -0,0 +1,83 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../skwasm_impl.dart';
/// A [Rasterizer] that uses a single GL context in an OffscreenCanvas to do
/// all the rendering. It transfers bitmaps created in the OffscreenCanvas to
/// one or many on-screen <canvas> elements to actually display the scene.
class SkwasmOffscreenCanvasRasterizer extends Rasterizer {
SkwasmOffscreenCanvasRasterizer(this.offscreenSurface);
/// This is an SkSurface backed by an OffScreenCanvas. This single Surface is
/// used to render to many RenderCanvases to produce the rendered scene.
final SkwasmSurface offscreenSurface;
@override
SkwasmOffscreenCanvasViewRasterizer createViewRasterizer(EngineFlutterView view) {
return _viewRasterizers.putIfAbsent(
view,
() => SkwasmOffscreenCanvasViewRasterizer(view, this),
);
}
final Map<EngineFlutterView, SkwasmOffscreenCanvasViewRasterizer> _viewRasterizers =
<EngineFlutterView, SkwasmOffscreenCanvasViewRasterizer>{};
@override
void setResourceCacheMaxBytes(int bytes) {
offscreenSurface.setSkiaResourceCacheMaxBytes(bytes);
}
@override
void dispose() {
offscreenSurface.dispose();
for (final SkwasmOffscreenCanvasViewRasterizer viewRasterizer in _viewRasterizers.values) {
viewRasterizer.dispose();
}
}
}
class SkwasmOffscreenCanvasViewRasterizer extends ViewRasterizer {
SkwasmOffscreenCanvasViewRasterizer(super.view, this.rasterizer);
final SkwasmOffscreenCanvasRasterizer rasterizer;
@override
final DisplayCanvasFactory<RenderCanvas> displayFactory = DisplayCanvasFactory<RenderCanvas>(
createCanvas: () => RenderCanvas(),
);
@override
void prepareToDraw() {
// No need to do anything here. Skwasm sizes the surface in the `rasterize`
// call below.
}
@override
Future<void> rasterize(
List<DisplayCanvas> displayCanvases,
List<ui.Picture> pictures,
FrameTimingRecorder? recorder,
) async {
if (displayCanvases.length != pictures.length) {
throw ArgumentError('Called rasterize() with a different number of canvases and pictures.');
}
final RenderResult renderResult = await rasterizer.offscreenSurface.renderPictures(
pictures.cast<SkwasmPicture>(),
currentFrameSize.width,
currentFrameSize.height,
);
recorder?.recordRasterStart(renderResult.rasterStartMicros);
recorder?.recordRasterFinish(renderResult.rasterEndMicros);
for (int i = 0; i < displayCanvases.length; i++) {
final RenderCanvas renderCanvas = displayCanvases[i] as RenderCanvas;
final DomImageBitmap bitmap = renderResult.imageBitmaps[i];
renderCanvas.render(bitmap);
}
}
}

View File

@ -14,7 +14,7 @@ enum PathDirection { clockwise, counterClockwise }
enum PathArcSize { small, large }
class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath, DisposablePath {
class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements LayerPath, DisposablePath {
factory SkwasmPath() {
return SkwasmPath.fromHandle(pathCreate());
}

View File

@ -6,7 +6,7 @@ import 'package:ui/src/engine.dart';
import 'package:ui/src/engine/skwasm/skwasm_impl.dart';
import 'package:ui/ui.dart' as ui;
class SkwasmPicture extends SkwasmObjectWrapper<RawPicture> implements ScenePicture {
class SkwasmPicture extends SkwasmObjectWrapper<RawPicture> implements LayerPicture {
SkwasmPicture.fromHandle(PictureHandle handle) : super(handle, _registry);
static final SkwasmFinalizationRegistry<RawPicture> _registry =
@ -39,7 +39,7 @@ class SkwasmPicture extends SkwasmObjectWrapper<RawPicture> implements ScenePict
}
class SkwasmPictureRecorder extends SkwasmObjectWrapper<RawPictureRecorder>
implements ScenePictureRecorder {
implements LayerPictureRecorder {
SkwasmPictureRecorder() : super(pictureRecorderCreate(), _registry);
static final SkwasmFinalizationRegistry<RawPictureRecorder> _registry =

View File

@ -63,6 +63,9 @@ external void canvasClipPath(CanvasHandle canvas, PathHandle path, bool antialia
@Native<Void Function(CanvasHandle, Int32, Int)>(symbol: 'canvas_drawColor', isLeaf: true)
external void canvasDrawColor(CanvasHandle canvas, int color, int blendMode);
@Native<Void Function(CanvasHandle, Int32)>(symbol: 'canvas_clear', isLeaf: true)
external void canvasClear(CanvasHandle canvas, int color);
@Native<Void Function(CanvasHandle, Float, Float, Float, Float, PaintHandle)>(
symbol: 'canvas_drawLine',
isLeaf: true,
@ -255,3 +258,6 @@ external void canvasGetLocalClipBounds(CanvasHandle canvas, RawRect outRect);
@Native<Void Function(CanvasHandle, RawIRect)>(symbol: 'canvas_getDeviceClipBounds', isLeaf: true)
external void canvasGetDeviceClipBounds(CanvasHandle canvas, RawIRect outRect);
@Native<Bool Function(CanvasHandle, RawRect)>(symbol: 'canvas_quickReject', isLeaf: true)
external bool canvasQuickReject(CanvasHandle canvas, RawRect rect);

View File

@ -33,6 +33,12 @@ external void surfaceSetCallbackHandler(SurfaceHandle surface, OnRenderCallbackH
@Native<Void Function(SurfaceHandle)>(symbol: 'surface_destroy', isLeaf: true)
external void surfaceDestroy(SurfaceHandle surface);
@Native<Void Function(SurfaceHandle, Int)>(
symbol: 'surface_setResourceCacheLimitBytes',
isLeaf: true,
)
external void surfaceSetResourceCacheLimitBytes(SurfaceHandle surface, int bytes);
@Native<Int32 Function(SurfaceHandle, Pointer<PictureHandle>, Int, Int, Int)>(
symbol: 'surface_renderPictures',
isLeaf: true,

View File

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:js_interop';
import 'dart:math' as math;
@ -16,8 +15,6 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
class SkwasmRenderer extends Renderer {
late SkwasmSurface surface;
final Map<EngineFlutterView, EngineSceneView> _sceneViews =
<EngineFlutterView, EngineSceneView>{};
bool get isMultiThreaded => skwasmIsMultiThreaded();
@ -178,7 +175,7 @@ class SkwasmRenderer extends Renderer {
);
@override
ui.SceneBuilder createSceneBuilder() => EngineSceneBuilder();
ui.SceneBuilder createSceneBuilder() => LayerSceneBuilder();
@override
ui.StrutStyle createStrutStyle({
@ -324,7 +321,7 @@ class SkwasmRenderer extends Renderer {
@override
FutureOr<void> initialize() {
surface = SkwasmSurface();
rasterizer = NoopRasterizer();
rasterizer = SkwasmOffscreenCanvasRasterizer(surface);
return super.initialize();
}
@ -399,25 +396,6 @@ class SkwasmRenderer extends Renderer {
}
}
@override
Future<void> renderScene(ui.Scene scene, EngineFlutterView view) {
final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled
? FrameTimingRecorder()
: null;
recorder?.recordBuildFinish();
final EngineSceneView sceneView = _getSceneViewForView(view);
return sceneView.renderScene(scene as EngineScene, recorder);
}
EngineSceneView _getSceneViewForView(EngineFlutterView view) {
return _sceneViews.putIfAbsent(view, () {
final EngineSceneView sceneView = EngineSceneView(SkwasmPictureRenderer(surface), view);
view.dom.setScene(sceneView.sceneElement);
return sceneView;
});
}
@override
String get rendererTag => 'skwasm';
@ -494,27 +472,6 @@ class SkwasmRenderer extends Renderer {
);
}
String _generateDebugFilename(String filePrefix) {
final now = DateTime.now();
final String y = now.year.toString().padLeft(4, '0');
final String mo = now.month.toString().padLeft(2, '0');
final String d = now.day.toString().padLeft(2, '0');
final String h = now.hour.toString().padLeft(2, '0');
final String mi = now.minute.toString().padLeft(2, '0');
final String s = now.second.toString().padLeft(2, '0');
return '$filePrefix-$y-$mo-$d-$h-$mi-$s.json';
}
void _dumpDebugInfo(String filePrefix, Map<String, dynamic> json) {
final String jsonString = const JsonEncoder.withIndent(' ').convert(json);
final blob = createDomBlob([jsonString], {'type': 'application/json'});
final url = domWindow.URL.createObjectURL(blob);
final element = domDocument.createElement('a');
element.setAttribute('href', url);
element.setAttribute('download', _generateDebugFilename(filePrefix));
element.click();
}
@override
void dumpDebugInfo() {
if (kDebugMode) {
@ -551,14 +508,14 @@ class SkwasmRenderer extends Renderer {
'surfaceCount': counts[26],
'verticesCount': counts[27],
};
_dumpDebugInfo('live_object_counts', countsJson);
downloadDebugInfo('live_object_counts', countsJson);
});
int i = 0;
for (final view in _sceneViews.values) {
final Map<String, dynamic>? debugJson = view.dumpDebugInfo();
for (final viewRasterizer in rasterizers.values) {
final Map<String, dynamic>? debugJson = viewRasterizer.dumpDebugInfo();
if (debugJson != null) {
_dumpDebugInfo('flutter-scene$i', debugJson);
downloadDebugInfo('flutter-scene$i', debugJson);
i++;
}
}
@ -567,106 +524,6 @@ class SkwasmRenderer extends Renderer {
@override
void debugResetRasterizer() {
rasterizer = NoopRasterizer();
rasterizer = SkwasmOffscreenCanvasRasterizer(surface);
}
}
// A [Rasterizer] that does nothing. A placeholder that will be fleshed out
// when Skwasm and CanvasKit renderers are unified. See: https://github.com/flutter/flutter/issues/172311
class NoopRasterizer implements Rasterizer {
@override
ViewRasterizer createViewRasterizer(EngineFlutterView view) {
return NoopViewRasterizer();
}
@override
void dispose() {
// Do nothing
}
@override
void setResourceCacheMaxBytes(int bytes) {
// Do nothing
}
}
// A [ViewRasterizer] that does nothing. A placeholder that will be fleshed out
// when Skwasm and CanvasKit renderers are unified. See: https://github.com/flutter/flutter/issues/172311
class NoopViewRasterizer implements ViewRasterizer {
@override
BitmapSize currentFrameSize = BitmapSize.zero;
@override
CompositorContext get context => throw UnimplementedError();
@override
void debugClear() {
// Do nothing
}
@override
DisplayCanvasFactory<DisplayCanvas> get displayFactory => throw UnimplementedError();
@override
void dispose() {
// Do nothing
}
@override
DisplayCanvas getOverlay() {
throw UnimplementedError();
}
@override
void prepareToDraw() {}
@override
RenderQueue get queue => throw UnimplementedError();
@override
void releaseOverlay(DisplayCanvas overlay) {}
@override
void releaseOverlays() {}
@override
void removeOverlaysFromDom() {}
@override
DomElement get sceneElement => throw UnimplementedError();
@override
EngineFlutterView get view => throw UnimplementedError();
@override
PlatformViewEmbedder get viewEmbedder => throw UnimplementedError();
@override
Map<String, dynamic>? dumpDebugInfo() {
throw UnimplementedError();
}
@override
Future<void> rasterize(
List<DisplayCanvas> displayCanvases,
List<ui.Picture> pictures,
FrameTimingRecorder? recorder,
) {
throw UnimplementedError();
}
@override
Future<void> draw(LayerTree layerTree, FrameTimingRecorder? recorder) {
throw UnimplementedError();
}
}
class SkwasmPictureRenderer implements PictureRenderer {
SkwasmPictureRenderer(this.surface);
SkwasmSurface surface;
@override
FutureOr<RenderResult> renderPictures(List<ScenePicture> pictures, int width, int height) =>
surface.renderPictures(pictures.cast<SkwasmPicture>(), width, height);
}

View File

@ -68,6 +68,12 @@ class SkwasmCallbackHandler {
}
}
typedef RenderResult = ({
List<DomImageBitmap> imageBitmaps,
int rasterStartMicros,
int rasterEndMicros,
});
class SkwasmSurface {
factory SkwasmSurface() {
final SurfaceHandle surfaceHandle = withStackScope((StackScope scope) {
@ -127,6 +133,10 @@ class SkwasmSurface {
return ByteData.sublistView(output);
}
void setSkiaResourceCacheMaxBytes(int bytes) {
surfaceSetResourceCacheLimitBytes(handle, bytes);
}
void dispose() {
surfaceDestroy(handle);
}

View File

@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:math' as math;
import 'dart:typed_data';
@ -798,3 +799,24 @@ class BitmapSize {
static const BitmapSize zero = BitmapSize(0, 0);
}
String _generateDebugFilename(String filePrefix) {
final now = DateTime.now();
final String y = now.year.toString().padLeft(4, '0');
final String mo = now.month.toString().padLeft(2, '0');
final String d = now.day.toString().padLeft(2, '0');
final String h = now.hour.toString().padLeft(2, '0');
final String mi = now.minute.toString().padLeft(2, '0');
final String s = now.second.toString().padLeft(2, '0');
return '$filePrefix-$y-$mo-$d-$h-$mi-$s.json';
}
void downloadDebugInfo(String filePrefix, Map<String, dynamic> json) {
final String jsonString = const JsonEncoder.withIndent(' ').convert(json);
final blob = createDomBlob([jsonString], {'type': 'application/json'});
final url = domWindow.URL.createObjectURL(blob);
final element = domDocument.createElement('a');
element.setAttribute('href', url);
element.setAttribute('download', _generateDebugFilename(filePrefix));
element.click();
}

View File

@ -172,6 +172,10 @@ SKWASM_EXPORT void canvas_transform(DisplayListBuilder* canvas,
canvas->Transform(*matrix44);
}
SKWASM_EXPORT void canvas_clear(DisplayListBuilder* canvas, uint32_t color) {
canvas->DrawColor(DlColor(color), DlBlendMode::kSrc);
}
SKWASM_EXPORT void canvas_clipRect(DisplayListBuilder* canvas,
const DlRect* rect,
DlClipOp op,
@ -365,3 +369,8 @@ SKWASM_EXPORT void canvas_getDeviceClipBounds(DisplayListBuilder* canvas,
DlIRect* outRect) {
*outRect = DlIRect::RoundOut(canvas->GetDestinationClipCoverage());
}
SKWASM_EXPORT bool canvas_quickReject(DisplayListBuilder* canvas,
DlRect* rect) {
return canvas->QuickReject(*rect);
}

View File

@ -42,6 +42,11 @@ void Surface::dispose() {
delete this;
}
// Main thread only
void Surface::setResourceCacheLimit(int bytes) {
_grContext->setResourceCacheLimit(bytes);
}
// Main thread only
uint32_t Surface::renderPictures(DisplayList** pictures,
int width,
@ -268,6 +273,11 @@ SKWASM_EXPORT void surface_dispose(Surface* surface) {
surface->dispose();
}
SKWASM_EXPORT void surface_setResourceCacheLimitBytes(Surface* surface,
int bytes) {
surface->setResourceCacheLimit(bytes);
}
SKWASM_EXPORT uint32_t surface_renderPictures(Surface* surface,
DisplayList** pictures,
int width,

View File

@ -58,6 +58,7 @@ class Surface {
// Main thread only
void dispose();
void setResourceCacheLimit(int bytes);
uint32_t renderPictures(flutter::DisplayList** picture,
int width,
int height,

View File

@ -1,380 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'scene_builder_utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUpAll(() {
LayerSliceBuilder.debugRecorderFactory = (ui.Rect rect) {
final StubSceneCanvas canvas = StubSceneCanvas();
final StubPictureRecorder recorder = StubPictureRecorder(canvas);
return (recorder, canvas);
};
});
tearDownAll(() {
LayerSliceBuilder.debugRecorderFactory = null;
});
group('EngineSceneBuilder', () {
test('single picture', () {
final EngineSceneBuilder sceneBuilder = EngineSceneBuilder();
const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect));
final EngineScene scene = sceneBuilder.build() as EngineScene;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 1);
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, 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<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 1);
expect(slices[0], layerSlice(withPictureRect: const ui.Rect.fromLTRB(100, 100, 400, 400)));
});
test('picture + platform view (overlapping)', () {
final EngineSceneBuilder sceneBuilder = EngineSceneBuilder();
const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200);
const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect));
sceneBuilder.addPlatformView(
1,
offset: platformViewRect.topLeft,
width: platformViewRect.width,
height: platformViewRect.height,
);
final EngineScene scene = sceneBuilder.build() as EngineScene;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 1);
expect(
slices[0],
layerSlice(
withPictureRect: pictureRect,
withPlatformViews: <PlatformView>[
PlatformView(1, platformViewRect, const PlatformViewStyling()),
],
),
);
});
test('platform view + picture (overlapping)', () {
final EngineSceneBuilder sceneBuilder = EngineSceneBuilder();
const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200);
const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250);
sceneBuilder.addPlatformView(
1,
offset: platformViewRect.topLeft,
width: platformViewRect.width,
height: platformViewRect.height,
);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect));
final EngineScene scene = sceneBuilder.build() as EngineScene;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(
slices[0],
layerSlice(
withPlatformViews: <PlatformView>[
PlatformView(1, platformViewRect, const PlatformViewStyling()),
],
),
);
expect(slices[1], layerSlice(withPictureRect: pictureRect));
});
test('platform view sandwich (overlapping)', () {
final EngineSceneBuilder sceneBuilder = EngineSceneBuilder();
const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200);
const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250);
const ui.Rect pictureRect2 = ui.Rect.fromLTRB(200, 200, 300, 300);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1));
sceneBuilder.addPlatformView(
1,
offset: platformViewRect.topLeft,
width: platformViewRect.width,
height: platformViewRect.height,
);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2));
final EngineScene scene = sceneBuilder.build() as EngineScene;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(
slices[0],
layerSlice(
withPictureRect: pictureRect1,
withPlatformViews: <PlatformView>[
PlatformView(1, platformViewRect, const PlatformViewStyling()),
],
),
);
expect(slices[1], layerSlice(withPictureRect: pictureRect2));
});
test('platform view sandwich (non-overlapping)', () {
final EngineSceneBuilder sceneBuilder = EngineSceneBuilder();
const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200);
const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250);
const ui.Rect pictureRect2 = ui.Rect.fromLTRB(50, 50, 100, 100);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1));
sceneBuilder.addPlatformView(
1,
offset: platformViewRect.topLeft,
width: platformViewRect.width,
height: platformViewRect.height,
);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2));
final EngineScene scene = sceneBuilder.build() as EngineScene;
final List<LayerSlice?> 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, 1);
expect(
slices[0],
layerSlice(
withPictureRect: const ui.Rect.fromLTRB(50, 50, 200, 200),
withPlatformViews: <PlatformView>[
PlatformView(1, platformViewRect, const PlatformViewStyling()),
],
),
);
});
test('platform view sandwich (overlapping) with offset layers', () {
final EngineSceneBuilder sceneBuilder = EngineSceneBuilder();
const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1));
sceneBuilder.pushOffset(150, 150);
const ui.Rect platformViewRect = ui.Rect.fromLTRB(0, 0, 100, 100);
sceneBuilder.addPlatformView(
1,
offset: platformViewRect.topLeft,
width: platformViewRect.width,
height: platformViewRect.height,
);
sceneBuilder.pushOffset(50, 50);
sceneBuilder.addPicture(ui.Offset.zero, StubPicture(const ui.Rect.fromLTRB(0, 0, 100, 100)));
final EngineScene scene = sceneBuilder.build() as EngineScene;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(
slices[0],
layerSlice(
withPictureRect: pictureRect1,
withPlatformViews: <PlatformView>[
PlatformView(
1,
platformViewRect,
const PlatformViewStyling(position: PlatformViewPosition.offset(ui.Offset(150, 150))),
),
],
),
);
expect(slices[1], layerSlice(withPictureRect: const ui.Rect.fromLTRB(200, 200, 300, 300)));
});
test('nested offset and clip', () {
final EngineSceneBuilder builder = EngineSceneBuilder();
{
builder.pushOffset(16, 43);
{
builder.pushClipRect(
const ui.Rect.fromLTRB(0, 0, 1007, 1122),
clipBehavior: ui.Clip.hardEdge,
);
{
builder.pushOffset(1, 214);
builder.addPlatformView(1, width: 1005, height: 907);
builder.pop();
}
builder.pop();
}
builder.pop();
}
const ui.Rect pictureRect = ui.Rect.fromLTRB(16, 198, 310, 280);
builder.addPicture(ui.Offset.zero, StubPicture(pictureRect));
final EngineScene scene = builder.build() as EngineScene;
final List<LayerSlice?> slices = scene.rootLayer.slices;
expect(slices.length, 2);
expect(
slices[0],
layerSlice(
withPlatformViews: <PlatformView>[
PlatformView(
1,
const ui.Rect.fromLTRB(0, 0, 1005, 907),
const PlatformViewStyling(
position: PlatformViewPosition.offset(ui.Offset(17, 257)),
clip: PlatformViewRectClip(ui.Rect.fromLTRB(16.0, 43.0, 1023.0, 1165.0)),
),
),
],
),
);
expect(slices[1], layerSlice(withPictureRect: pictureRect));
});
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<PlatformView> expectedPlatformViews = <PlatformView>[];
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<LayerSlice?> 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,
),
),
);
});
});
}
LayerSliceMatcher layerSlice({
ui.Rect withPictureRect = ui.Rect.zero,
List<PlatformView> withPlatformViews = const <PlatformView>[],
}) => LayerSliceMatcher(withPictureRect, withPlatformViews);
class LayerSliceMatcher extends Matcher {
LayerSliceMatcher(this.expectedPictureRect, this.expectedPlatformViews);
final ui.Rect expectedPictureRect;
final List<PlatformView> expectedPlatformViews;
@override
Description describe(Description description) {
return description.add(
'<picture slice with cullRect: $expectedPictureRect and platform views: $expectedPlatformViews>',
);
}
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
if (item is! LayerSlice) {
return false;
}
final ScenePicture picture = item.picture;
if (picture is! StubPicture) {
return false;
}
if (picture.cullRect != expectedPictureRect) {
return false;
}
if (item.platformViews.length != expectedPlatformViews.length) {
return false;
}
for (int i = 0; i < item.platformViews.length; i++) {
final PlatformView expectedView = expectedPlatformViews[i];
final PlatformView actualView = item.platformViews[i];
if (expectedView.viewId != actualView.viewId) {
return false;
}
if (expectedView.bounds != actualView.bounds) {
return false;
}
if (expectedView.styling != actualView.styling) {
return false;
}
}
return true;
}
}

View File

@ -1,243 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
class StubPicture implements ScenePicture {
StubPicture(this.cullRect);
bool _isDisposed = false;
@override
final ui.Rect cullRect;
@override
int get approximateBytesUsed => throw UnimplementedError();
@override
bool get debugDisposed => _isDisposed;
@override
void dispose() {
assert(!_isDisposed);
_isDisposed = true;
}
@override
Future<ui.Image> toImage(int width, int height) {
throw UnimplementedError();
}
@override
ui.Image toImageSync(int width, int height) {
throw UnimplementedError();
}
}
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<StubPicture> children;
}
class StubPictureRecorder implements ui.PictureRecorder {
StubPictureRecorder(this.canvas);
final StubSceneCanvas canvas;
@override
ui.Picture endRecording() {
return StubCompositePicture(canvas.pictures);
}
@override
bool get isRecording => throw UnimplementedError();
}
class StubSceneCanvas implements SceneCanvas {
List<StubPicture> pictures = <StubPicture>[];
// We actually use offsets in some of the tests, so we need to track the
// translate calls as they are made.
List<ui.Offset> offsetStack = <ui.Offset>[ui.Offset.zero];
ui.Offset get currentOffset {
return offsetStack.last;
}
set currentOffset(ui.Offset offset) {
offsetStack[offsetStack.length - 1] = offset;
}
@override
void drawPicture(ui.Picture picture) {
pictures.add(StubPicture((picture as StubPicture).cullRect.shift(currentOffset)));
}
@override
void clipPath(ui.Path path, {bool doAntiAlias = true}) {}
@override
void clipRRect(ui.RRect rrect, {bool doAntiAlias = true}) {}
@override
void clipRSuperellipse(ui.RSuperellipse rsuperellipse, {bool doAntiAlias = true}) {}
@override
void clipRect(ui.Rect rect, {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) {}
@override
void drawArc(
ui.Rect rect,
double startAngle,
double sweepAngle,
bool useCenter,
ui.Paint paint,
) {}
@override
void drawAtlas(
ui.Image atlas,
List<ui.RSTransform> transforms,
List<ui.Rect> rects,
List<ui.Color>? colors,
ui.BlendMode? blendMode,
ui.Rect? cullRect,
ui.Paint paint,
) {}
@override
void drawCircle(ui.Offset c, double radius, ui.Paint paint) {}
@override
void drawColor(ui.Color color, ui.BlendMode blendMode) {}
@override
void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) {}
@override
void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) {}
@override
void drawImageNine(ui.Image image, ui.Rect center, ui.Rect dst, ui.Paint paint) {}
@override
void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, ui.Paint paint) {}
@override
void drawLine(ui.Offset p1, ui.Offset p2, ui.Paint paint) {}
@override
void drawOval(ui.Rect rect, ui.Paint paint) {}
@override
void drawPaint(ui.Paint paint) {}
@override
void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {}
@override
void drawPath(ui.Path path, ui.Paint paint) {}
@override
void drawPoints(ui.PointMode pointMode, List<ui.Offset> points, ui.Paint paint) {}
@override
void drawRRect(ui.RRect rrect, ui.Paint paint) {}
@override
void drawRSuperellipse(ui.RSuperellipse rsuperellipse, ui.Paint paint) {}
@override
void drawRawAtlas(
ui.Image atlas,
Float32List rstTransforms,
Float32List rects,
Int32List? colors,
ui.BlendMode? blendMode,
ui.Rect? cullRect,
ui.Paint paint,
) {}
@override
void drawRawPoints(ui.PointMode pointMode, Float32List points, ui.Paint paint) {}
@override
void drawRect(ui.Rect rect, ui.Paint paint) {}
@override
void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) {}
@override
void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, ui.Paint paint) {}
@override
ui.Rect getDestinationClipBounds() {
throw UnimplementedError();
}
@override
ui.Rect getLocalClipBounds() {
throw UnimplementedError();
}
@override
int getSaveCount() {
throw UnimplementedError();
}
@override
Float64List getTransform() {
throw UnimplementedError();
}
@override
void restore() {
offsetStack.removeLast();
}
@override
void restoreToCount(int count) {}
@override
void rotate(double radians) {}
@override
void save() {
offsetStack.add(currentOffset);
}
@override
void saveLayer(ui.Rect? bounds, ui.Paint paint) {}
@override
void saveLayerWithFilter(ui.Rect? bounds, ui.Paint paint, ui.ImageFilter backdropFilter) {}
@override
void scale(double sx, [double? sy]) {}
@override
void skew(double sx, double sy) {}
@override
void transform(Float64List matrix4) {}
@override
void translate(double dx, double dy) {
currentOffset += ui.Offset(dx, dy);
}
}

View File

@ -1,413 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart';
import '../common/test_initialization.dart';
import 'scene_builder_utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
class StubPictureRenderer implements PictureRenderer {
final DomHTMLCanvasElement scratchCanvasElement = createDomCanvasElement(width: 500, height: 500);
@override
Future<RenderResult> renderPictures(List<ScenePicture> pictures, int width, int height) async {
renderedPictures.addAll(pictures);
final List<DomImageBitmap> bitmaps = await Future.wait(
pictures.map((ScenePicture picture) {
final ui.Rect cullRect = picture.cullRect;
final Future<DomImageBitmap> bitmap = createImageBitmap(scratchCanvasElement, (
x: 0,
y: 0,
width: cullRect.width.toInt(),
height: cullRect.height.toInt(),
));
return bitmap;
}),
);
return (imageBitmaps: bitmaps, rasterStartMicros: 0, rasterEndMicros: 0);
}
List<ScenePicture> renderedPictures = <ScenePicture>[];
List<StubPicture> clippedPictures = <StubPicture>[];
}
class StubFlutterView implements EngineFlutterView {
// Overridden in some tests
@override
DomManager dom = StubDomManager();
@override
double get devicePixelRatio => throw UnimplementedError();
@override
EngineFlutterDisplay get display => throw UnimplementedError();
@override
List<ui.DisplayFeature> get displayFeatures => throw UnimplementedError();
@override
ui.GestureSettings get gestureSettings => throw UnimplementedError();
@override
ViewPadding get padding => throw UnimplementedError();
@override
ViewConstraints get physicalConstraints => throw UnimplementedError();
@override
ui.Size get physicalSize => const ui.Size(1000, 1000);
@override
EnginePlatformDispatcher get platformDispatcher => throw UnimplementedError();
@override
void render(ui.Scene scene, {ui.Size? size}) {}
@override
ViewPadding get systemGestureInsets => throw UnimplementedError();
@override
void updateSemantics(ui.SemanticsUpdate update) {}
@override
int get viewId => throw UnimplementedError();
@override
ViewPadding get viewInsets => throw UnimplementedError();
@override
ViewPadding get viewPadding => throw UnimplementedError();
@override
ui.Size? debugPhysicalSizeOverride;
@override
bool isDisposed = false;
@override
PointerBinding get pointerBinding => throw UnimplementedError();
@override
set pointerBinding(_) {
throw UnimplementedError();
}
@override
ContextMenu get contextMenu => throw UnimplementedError();
@override
void debugForceResize() {
throw UnimplementedError();
}
@override
DimensionsProvider get dimensionsProvider => throw UnimplementedError();
@override
void dispose() {
throw UnimplementedError();
}
@override
EmbeddingStrategy get embeddingStrategy => throw UnimplementedError();
@override
MouseCursor get mouseCursor => throw UnimplementedError();
@override
Stream<ui.Size?> get onResize => throw UnimplementedError();
@override
void resize(ui.Size newPhysicalSize) {
throw UnimplementedError();
}
@override
EngineSemanticsOwner get semantics => throw UnimplementedError();
}
class StubPath implements ScenePath {
StubPath(this.pathString, this.transformedPathString);
final String pathString;
final String transformedPathString;
Float64List? appliedTransform;
@override
ui.PathFillType get fillType => throw UnimplementedError();
@override
set fillType(ui.PathFillType value) => throw UnimplementedError();
@override
void moveTo(double x, double y) => throw UnimplementedError();
@override
void relativeMoveTo(double dx, double dy) => throw UnimplementedError();
@override
void lineTo(double x, double y) => throw UnimplementedError();
@override
void relativeLineTo(double dx, double dy) => throw UnimplementedError();
@override
void quadraticBezierTo(double x1, double y1, double x2, double y2) => throw UnimplementedError();
@override
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) =>
throw UnimplementedError();
@override
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) =>
throw UnimplementedError();
@override
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) =>
throw UnimplementedError();
@override
void conicTo(double x1, double y1, double x2, double y2, double w) => throw UnimplementedError();
@override
void relativeConicTo(double x1, double y1, double x2, double y2, double w) =>
throw UnimplementedError();
@override
void arcTo(ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) =>
throw UnimplementedError();
@override
void arcToPoint(
ui.Offset arcEnd, {
ui.Radius radius = ui.Radius.zero,
double rotation = 0.0,
bool largeArc = false,
bool clockwise = true,
}) => throw UnimplementedError();
@override
void relativeArcToPoint(
ui.Offset arcEndDelta, {
ui.Radius radius = ui.Radius.zero,
double rotation = 0.0,
bool largeArc = false,
bool clockwise = true,
}) => throw UnimplementedError();
@override
void addRect(ui.Rect rect) => throw UnimplementedError();
@override
void addOval(ui.Rect oval) => throw UnimplementedError();
@override
void addArc(ui.Rect oval, double startAngle, double sweepAngle) => throw UnimplementedError();
@override
void addPolygon(List<ui.Offset> points, bool close) => throw UnimplementedError();
@override
void addRRect(ui.RRect rrect) => throw UnimplementedError();
@override
void addRSuperellipse(ui.RSuperellipse rsuperellipse) => throw UnimplementedError();
@override
void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) =>
throw UnimplementedError();
@override
void extendWithPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) =>
throw UnimplementedError();
@override
void close() => throw UnimplementedError();
@override
void reset() => throw UnimplementedError();
@override
bool contains(ui.Offset point) => throw UnimplementedError();
@override
ui.Path shift(ui.Offset offset) => throw UnimplementedError();
@override
StubPath transform(Float64List matrix4) {
appliedTransform = matrix4;
return StubPath(transformedPathString, '');
}
@override
ui.Rect getBounds() => throw UnimplementedError();
@override
ui.PathMetrics computeMetrics({bool forceClosed = false}) => throw UnimplementedError();
@override
String toSvgString() => pathString;
}
void testMain() {
late EngineSceneView sceneView;
late StubPictureRenderer stubPictureRenderer;
setUpImplicitView();
setUp(() {
stubPictureRenderer = StubPictureRenderer();
sceneView = EngineSceneView(stubPictureRenderer, StubFlutterView());
});
tearDown(() {
expect(
stubPictureRenderer.clippedPictures.every((StubPicture picture) => picture.debugDisposed),
true,
);
});
test('SceneView places platform view according to device-pixel ratio', () async {
debugOverrideDevicePixelRatio(2.0);
final StubPath clipPath = StubPath('M 2 2', 'M 1 1');
final PlatformView platformView = PlatformView(
1,
const ui.Rect.fromLTWH(50, 80, 100, 120),
PlatformViewStyling(clip: PlatformViewPathClip(clipPath)),
);
final EngineRootLayer rootLayer = EngineRootLayer();
rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), <PlatformView>[platformView]));
final EngineScene scene = EngineScene(rootLayer);
await sceneView.renderScene(scene, null);
final DomElement sceneElement = sceneView.sceneElement;
final List<DomElement> children = sceneElement.children.toList();
expect(children.length, 1);
final DomElement clipElement = children.first;
expect(clipElement.tagName, equalsIgnoringCase('flt-clip'));
expect(clipElement.style.clipPath, 'path("M 1 1")');
// We expect the path to be scaled down by 1 / DPR
final Matrix4 expectedTransform = Matrix4.identity()..scale(0.5);
expect(clipPath.appliedTransform, expectedTransform.toFloat64());
final List<DomElement> clipChildren = clipElement.children.toList();
expect(clipChildren.length, 1);
final DomElement containerElement = clipChildren.first;
final DomCSSStyleDeclaration style = containerElement.style;
expect(style.left, '');
expect(style.top, '');
expect(style.width, '100px');
expect(style.height, '120px');
// The heavy lifting of offsetting and sizing is done by the transform
expect(style.transform, 'matrix(0.5, 0, 0, 0.5, 25, 40)');
debugOverrideDevicePixelRatio(null);
});
test('SceneView always renders most recent picture and skips intermediate pictures', () async {
final List<StubPicture> pictures = <StubPicture>[];
final List<Future<void>> renderFutures = <Future<void>>[];
for (int i = 1; i < 20; i++) {
final StubPicture picture = StubPicture(const ui.Rect.fromLTWH(50, 80, 100, 120));
pictures.add(picture);
final EngineRootLayer rootLayer = EngineRootLayer();
rootLayer.slices.add(LayerSlice(picture, <PlatformView>[]));
final EngineScene scene = EngineScene(rootLayer);
renderFutures.add(sceneView.renderScene(scene, null));
}
await Future.wait(renderFutures);
// Should just render the first and last pictures and skip the one inbetween.
expect(stubPictureRenderer.renderedPictures.length, 2);
expect(stubPictureRenderer.renderedPictures.first, pictures.first);
expect(stubPictureRenderer.renderedPictures.last, pictures.last);
});
test('SceneView clips pictures that are outside the window screen', () async {
final StubPicture picture = StubPicture(const ui.Rect.fromLTWH(-50, -50, 100, 120));
final EngineRootLayer rootLayer = EngineRootLayer();
rootLayer.slices.add(LayerSlice(picture, <PlatformView>[]));
final EngineScene scene = EngineScene(rootLayer);
await sceneView.renderScene(scene, null);
expect(stubPictureRenderer.renderedPictures.length, 1);
});
test('SceneView places platform view contents in the DOM', () async {
const int expectedPlatformViewId = 1234;
int? injectedViewId;
final DomManager stubDomManager = StubDomManager()
..injectPlatformViewOverride = (int viewId) {
injectedViewId = viewId;
};
sceneView = EngineSceneView(stubPictureRenderer, StubFlutterView()..dom = stubDomManager);
final PlatformView platformView = PlatformView(
expectedPlatformViewId,
const ui.Rect.fromLTWH(50, 80, 100, 120),
const PlatformViewStyling(),
);
final EngineRootLayer rootLayer = EngineRootLayer();
rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), <PlatformView>[platformView]));
final EngineScene scene = EngineScene(rootLayer);
await sceneView.renderScene(scene, null);
expect(
injectedViewId,
expectedPlatformViewId,
reason: 'SceneView should call injectPlatformView on its flutterView.dom',
);
});
}
class StubDomManager implements DomManager {
void Function(int platformViewId) injectPlatformViewOverride = (int id) {};
@override
void injectPlatformView(int platformViewId) {
injectPlatformViewOverride(platformViewId);
}
@override
DomElement get platformViewsHost => throw UnimplementedError();
@override
DomShadowRoot get renderingHost => throw UnimplementedError();
@override
DomElement get rootElement => throw UnimplementedError();
@override
DomElement get sceneHost => throw UnimplementedError();
@override
DomElement get semanticsHost => throw UnimplementedError();
@override
void setScene(DomElement sceneElement) {}
@override
DomElement get textEditingHost => throw UnimplementedError();
}

View File

@ -38,9 +38,7 @@ void testMain() {
await drawTestPicture(canvas as LayerCanvas);
await drawPictureUsingCurrentRenderer(recorder.endRecording());
await matchGoldenFile('ui_weakref_picture.png', region: kDefaultRegion);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('text style - foreground/background/color do not leak across paragraphs', () async {
const double testWidth = 440;

View File

@ -10,7 +10,6 @@ import 'package:web_engine_tester/golden_tester.dart';
import '../common/rendering.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@ -292,9 +291,7 @@ void testMain() {
await renderScene(builder.build());
await matchGoldenFile('ui_colorfilter_as_imagefilter.png', region: region);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm || isSafari || isFirefox);
});
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520
}, skip: isSafari || isFirefox);
}

View File

@ -486,8 +486,8 @@ Future<void> testMain() async {
await renderScene(builder.build());
await matchGoldenFile('ui_zero_sigma_blur.png', region: region);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
// Skwasm crashes with a zero-sigma blur. See:
// https://github.com/flutter/flutter/issues/174583
}, skip: isSkwasm);
test('== operator', () {

View File

@ -135,7 +135,5 @@ void testMain() {
region: const ui.Rect.fromLTRB(0, 0, 200, 200),
);
});
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
}

View File

@ -54,9 +54,7 @@ void testMain() {
createDomElement('unregistered-view'),
);
expect(() => renderer.renderScene(scene, unregisteredView), throwsAssertionError);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('will dispose the Rasterizer for a disposed view', () async {
final EngineFlutterView view = EngineFlutterView(

View File

@ -8,7 +8,6 @@ import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@ -33,9 +32,7 @@ Future<void> testMain() async {
picture.dispose();
expect(picture.debugDisposed, true);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('Picture construction invokes onCreate once', () async {
int onCreateInvokedCount = 0;

View File

@ -483,9 +483,7 @@ Future<void> testMain() async {
'matrix(1, 0, 0, 1, 6, 6)',
'matrix(1, 0, 0, 1, 3, 3)',
]);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('converts device pixels to logical pixels (no clips)', () async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(4);
@ -503,9 +501,7 @@ Future<void> testMain() async {
expect(getTransformChain(slotHost), <String>['matrix(0.25, 0, 0, 0.25, 1.5, 1.5)']);
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('converts device pixels to logical pixels (with clips)', () async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(4);
@ -529,9 +525,7 @@ Future<void> testMain() async {
'matrix(0.25, 0, 0, 0.25, 0.75, 0.75)',
]);
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('renders overlays on top of platform views', () async {
debugOverrideJsConfiguration(
@ -677,9 +671,7 @@ Future<void> testMain() async {
await renderTestScene(viewCount: 0);
_expectSceneMatches(<_EmbeddedViewMarker>[]);
debugOverrideJsConfiguration(null);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('correctly reuses overlays', () async {
final ui.PictureRecorder testRecorder = ui.PictureRecorder();
@ -803,9 +795,7 @@ Future<void> testMain() async {
_overlay,
_platformView,
]);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('embeds and disposes of a platform view', () async {
await createPlatformView(1, platformViewType);
@ -899,10 +889,8 @@ Future<void> testMain() async {
implicitView.debugForceResize();
// ImageDecoder is not supported in Safari or Firefox.
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
},
skip: isSafari || isFirefox || isSkwasm,
skip: isSafari || isFirefox,
);
test('does not crash when a prerolled platform view is not composited', () async {
@ -916,9 +904,7 @@ Future<void> testMain() async {
// The below line should not throw an error.
await renderScene(sb.build());
_expectSceneMatches(<_EmbeddedViewMarker>[]);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('does not create overlays for invisible platform views', () async {
final ui.PictureRecorder testRecorder = ui.PictureRecorder();
@ -1150,9 +1136,7 @@ Future<void> testMain() async {
_platformView,
_overlay,
]);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('can dispose without crashing', () async {
ui_web.platformViewRegistry.registerViewFactory(
@ -1181,9 +1165,7 @@ Future<void> testMain() async {
// The following line used to cause a "Concurrent modification during iteration"
embedder.dispose();
}, returnsNormally);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('optimizes overlays when pictures and platform views do not overlap', () async {
ui.Picture rectPicture(ui.Rect rect) {
@ -1422,10 +1404,7 @@ Future<void> testMain() async {
.map((CompositionCanvas canvas) => canvas.pictures.length)
.toList();
expect(picturesPerCanvas, <int>[1, 2]);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('can customize amount of overlays', () async {
final ui.PictureRecorder testRecorder = ui.PictureRecorder();
final ui.Canvas testCanvas = ui.Canvas(testRecorder);
@ -1493,9 +1472,7 @@ Future<void> testMain() async {
]);
debugOverrideJsConfiguration(null);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('correctly rearranges pictures to second-to-last canvas '
'when hitting canvas limit', () async {
@ -1605,10 +1582,7 @@ Future<void> testMain() async {
.toList();
expect(picturesPerCanvasInSecondRendering, <int>[19]);
debugOverrideJsConfiguration(null);
// Unskip when Skwasm and CanvasKit are unified:
// https://github.com/flutter/flutter/issues/172311
}, skip: isSkwasm);
});
test('disposes render pictures', () async {
Instrumentation.enabled = true;
Instrumentation.instance.debugCounters.clear();