mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[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:
parent
0628f86a78
commit
111f5a45f0
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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 = [];
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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.
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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', () {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user