mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Reland "Use a single OffscreenCanvas for rendering in CanvasKit (#45744)" (flutter/engine#47241)
Using a single GL context avoids several issues with managing GL context lifecycle. ## 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] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
parent
5e760248e0
commit
84e466b785
@ -2621,10 +2621,11 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.da
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart + ../../../flutter/LICENSE
|
||||
@ -5400,10 +5401,11 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart
|
||||
|
||||
@ -43,10 +43,11 @@ export 'engine/canvaskit/picture.dart';
|
||||
export 'engine/canvaskit/picture_recorder.dart';
|
||||
export 'engine/canvaskit/raster_cache.dart';
|
||||
export 'engine/canvaskit/rasterizer.dart';
|
||||
export 'engine/canvaskit/render_canvas.dart';
|
||||
export 'engine/canvaskit/render_canvas_factory.dart';
|
||||
export 'engine/canvaskit/renderer.dart';
|
||||
export 'engine/canvaskit/shader.dart';
|
||||
export 'engine/canvaskit/surface.dart';
|
||||
export 'engine/canvaskit/surface_factory.dart';
|
||||
export 'engine/canvaskit/text.dart';
|
||||
export 'engine/canvaskit/text_fragmenter.dart';
|
||||
export 'engine/canvaskit/util.dart';
|
||||
|
||||
@ -161,6 +161,13 @@ extension CanvasKitExtension on CanvasKit {
|
||||
DomCanvasElement canvas, SkWebGLContextOptions options) =>
|
||||
_GetWebGLContext(canvas, options).toDartDouble;
|
||||
|
||||
@JS('GetWebGLContext')
|
||||
external JSNumber _GetOffscreenWebGLContext(
|
||||
DomOffscreenCanvas canvas, SkWebGLContextOptions options);
|
||||
double GetOffscreenWebGLContext(
|
||||
DomOffscreenCanvas canvas, SkWebGLContextOptions options) =>
|
||||
_GetOffscreenWebGLContext(canvas, options).toDartDouble;
|
||||
|
||||
@JS('MakeGrContext')
|
||||
external SkGrContext _MakeGrContext(JSNumber glContext);
|
||||
SkGrContext MakeGrContext(double glContext) =>
|
||||
@ -199,6 +206,9 @@ extension CanvasKitExtension on CanvasKit {
|
||||
|
||||
external SkSurface MakeSWCanvasSurface(DomCanvasElement canvas);
|
||||
|
||||
@JS('MakeSWCanvasSurface')
|
||||
external SkSurface MakeOffscreenSWCanvasSurface(DomOffscreenCanvas canvas);
|
||||
|
||||
/// Creates an image from decoded pixels represented as a list of bytes.
|
||||
///
|
||||
/// The pixel data must be encoded according to the image info in [info].
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../engine.dart' show PlatformViewManager;
|
||||
import '../configuration.dart';
|
||||
import '../dom.dart';
|
||||
import '../html/path_to_svg_clip.dart';
|
||||
import '../platform_views/slots.dart';
|
||||
@ -18,9 +17,9 @@ import 'embedded_views_diff.dart';
|
||||
import 'path.dart';
|
||||
import 'picture.dart';
|
||||
import 'picture_recorder.dart';
|
||||
import 'render_canvas.dart';
|
||||
import 'render_canvas_factory.dart';
|
||||
import 'renderer.dart';
|
||||
import 'surface.dart';
|
||||
import 'surface_factory.dart';
|
||||
|
||||
/// This composites HTML views into the [ui.Scene].
|
||||
class HtmlViewEmbedder {
|
||||
@ -31,42 +30,6 @@ class HtmlViewEmbedder {
|
||||
|
||||
DomElement get skiaSceneHost => CanvasKitRenderer.instance.sceneHost!;
|
||||
|
||||
/// Force the view embedder to disable overlays.
|
||||
///
|
||||
/// This should never be used outside of tests.
|
||||
static set debugDisableOverlays(bool disable) {
|
||||
// Short circuit if the value is the same as what we already have.
|
||||
if (disable == _debugOverlaysDisabled) {
|
||||
return;
|
||||
}
|
||||
_debugOverlaysDisabled = disable;
|
||||
final SurfaceFactory? instance = SurfaceFactory.debugUninitializedInstance;
|
||||
if (instance != null) {
|
||||
instance.releaseSurfaces();
|
||||
instance.removeSurfacesFromDom();
|
||||
instance.debugClear();
|
||||
}
|
||||
if (disable) {
|
||||
// If we are disabling overlays then get the current [SurfaceFactory]
|
||||
// instance, clear it, and overwrite it with a new instance with only
|
||||
// one surface for the base surface.
|
||||
SurfaceFactory.debugSetInstance(SurfaceFactory(1));
|
||||
} else {
|
||||
// If we are re-enabling overlays then replace the current
|
||||
// [SurfaceFactory]instance with one with
|
||||
// [configuration.canvasKitMaximumSurfaces] overlays.
|
||||
SurfaceFactory.debugSetInstance(
|
||||
SurfaceFactory(configuration.canvasKitMaximumSurfaces));
|
||||
}
|
||||
}
|
||||
|
||||
static bool _debugOverlaysDisabled = false;
|
||||
|
||||
/// Whether or not we have issues a warning to the user about having too many
|
||||
/// surfaces on screen at once. This is so we only warn once, instead of every
|
||||
/// frame.
|
||||
bool _warnedAboutTooManySurfaces = false;
|
||||
|
||||
/// The context for the current frame.
|
||||
EmbedderFrameContext _context = EmbedderFrameContext();
|
||||
|
||||
@ -86,10 +49,12 @@ class HtmlViewEmbedder {
|
||||
/// * The number of clipping elements used last time the view was composited.
|
||||
final Map<int, ViewClipChain> _viewClipChains = <int, ViewClipChain>{};
|
||||
|
||||
/// Surfaces used to draw on top of platform views, keyed by platform view ID.
|
||||
///
|
||||
/// These surfaces are cached in the [OverlayCache] and reused.
|
||||
final Map<int, Surface> _overlays = <int, Surface>{};
|
||||
/// The maximum number of overlays to create. Too many overlays can cause a
|
||||
/// performance burden.
|
||||
static const int maximumOverlays = 7;
|
||||
|
||||
/// Canvases used to draw on top of platform views, keyed by platform view ID.
|
||||
final Map<int, RenderCanvas> _overlays = <int, RenderCanvas>{};
|
||||
|
||||
/// The views that need to be recomposited into the scene on the next frame.
|
||||
final Set<int> _viewsToRecomposite = <int>{};
|
||||
@ -100,6 +65,9 @@ class HtmlViewEmbedder {
|
||||
/// The most recent composition order.
|
||||
final List<int> _activeCompositionOrder = <int>[];
|
||||
|
||||
/// The most recent overlay groups.
|
||||
List<OverlayGroup> _activeOverlayGroups = <OverlayGroup>[];
|
||||
|
||||
/// The size of the frame, in physical pixels.
|
||||
ui.Size _frameSize = ui.window.physicalSize;
|
||||
|
||||
@ -124,20 +92,10 @@ class HtmlViewEmbedder {
|
||||
}
|
||||
|
||||
void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) {
|
||||
final bool hasAvailableOverlay =
|
||||
_context.pictureRecordersCreatedDuringPreroll.length <
|
||||
SurfaceFactory.instance.maximumOverlays;
|
||||
if (!hasAvailableOverlay && !_warnedAboutTooManySurfaces) {
|
||||
_warnedAboutTooManySurfaces = true;
|
||||
printWarning('Flutter was unable to create enough overlay surfaces. '
|
||||
'This is usually caused by too many platform views being '
|
||||
'displayed at once. '
|
||||
'You may experience incorrect rendering.');
|
||||
}
|
||||
// We need an overlay for each visible platform view. Invisible platform
|
||||
// views will be grouped with (at most) one visible platform view later.
|
||||
final bool needNewOverlay = PlatformViewManager.instance.isVisible(viewId);
|
||||
if (needNewOverlay && hasAvailableOverlay) {
|
||||
if (needNewOverlay) {
|
||||
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
|
||||
pictureRecorder.beginRecording(ui.Offset.zero & _frameSize);
|
||||
_context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder);
|
||||
@ -409,26 +367,27 @@ class HtmlViewEmbedder {
|
||||
(_activeCompositionOrder.isEmpty || _compositionOrder.isEmpty)
|
||||
? null
|
||||
: diffViewList(_activeCompositionOrder, _compositionOrder);
|
||||
_updateOverlays(diffResult);
|
||||
final List<OverlayGroup>? overlayGroups = _updateOverlays(diffResult);
|
||||
if (overlayGroups != null) {
|
||||
_activeOverlayGroups = overlayGroups;
|
||||
}
|
||||
assert(
|
||||
_context.pictureRecorders.length == _overlays.length,
|
||||
'There should be the same number of picture recorders '
|
||||
_context.pictureRecorders.length >= _overlays.length,
|
||||
'There should be at least as many picture recorders '
|
||||
'(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).',
|
||||
);
|
||||
int pictureRecorderIndex = 0;
|
||||
|
||||
for (int i = 0; i < _compositionOrder.length; i++) {
|
||||
final int viewId = _compositionOrder[i];
|
||||
if (_overlays[viewId] != null) {
|
||||
final SurfaceFrame frame = _overlays[viewId]!.acquireFrame(_frameSize);
|
||||
final CkCanvas canvas = frame.skiaCanvas;
|
||||
final CkPicture ckPicture =
|
||||
_context.pictureRecorders[pictureRecorderIndex].endRecording();
|
||||
canvas.clear(const ui.Color(0x00000000));
|
||||
canvas.drawPicture(ckPicture);
|
||||
int pictureRecorderIndex = 0;
|
||||
for (final OverlayGroup overlayGroup in _activeOverlayGroups) {
|
||||
final RenderCanvas overlay = _overlays[overlayGroup.last]!;
|
||||
final List<CkPicture> pictures = <CkPicture>[];
|
||||
for (int i = 0; i < overlayGroup.visibleCount; i++) {
|
||||
pictures.add(
|
||||
_context.pictureRecorders[pictureRecorderIndex].endRecording());
|
||||
pictureRecorderIndex++;
|
||||
frame.submit();
|
||||
}
|
||||
CanvasKitRenderer.instance.rasterizer
|
||||
.rasterizeToCanvas(overlay, pictures);
|
||||
}
|
||||
for (final CkPictureRecorder recorder
|
||||
in _context.pictureRecordersCreatedDuringPreroll) {
|
||||
@ -481,7 +440,7 @@ class HtmlViewEmbedder {
|
||||
if (diffResult.addToBeginning) {
|
||||
final DomElement platformViewRoot = _viewClipChains[viewId]!.root;
|
||||
skiaSceneHost.insertBefore(platformViewRoot, elementToInsertBefore);
|
||||
final Surface? overlay = _overlays[viewId];
|
||||
final RenderCanvas? overlay = _overlays[viewId];
|
||||
if (overlay != null) {
|
||||
skiaSceneHost.insertBefore(
|
||||
overlay.htmlElement, elementToInsertBefore);
|
||||
@ -489,7 +448,7 @@ class HtmlViewEmbedder {
|
||||
} else {
|
||||
final DomElement platformViewRoot = _viewClipChains[viewId]!.root;
|
||||
skiaSceneHost.append(platformViewRoot);
|
||||
final Surface? overlay = _overlays[viewId];
|
||||
final RenderCanvas? overlay = _overlays[viewId];
|
||||
if (overlay != null) {
|
||||
skiaSceneHost.append(overlay.htmlElement);
|
||||
}
|
||||
@ -514,7 +473,7 @@ class HtmlViewEmbedder {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SurfaceFactory.instance.removeSurfacesFromDom();
|
||||
RenderCanvasFactory.instance.removeSurfacesFromDom();
|
||||
for (int i = 0; i < _compositionOrder.length; i++) {
|
||||
final int viewId = _compositionOrder[i];
|
||||
|
||||
@ -532,7 +491,7 @@ class HtmlViewEmbedder {
|
||||
}
|
||||
|
||||
final DomElement platformViewRoot = _viewClipChains[viewId]!.root;
|
||||
final Surface? overlay = _overlays[viewId];
|
||||
final RenderCanvas? overlay = _overlays[viewId];
|
||||
skiaSceneHost.append(platformViewRoot);
|
||||
if (overlay != null) {
|
||||
skiaSceneHost.append(overlay.htmlElement);
|
||||
@ -568,8 +527,8 @@ class HtmlViewEmbedder {
|
||||
|
||||
void _releaseOverlay(int viewId) {
|
||||
if (_overlays[viewId] != null) {
|
||||
final Surface overlay = _overlays[viewId]!;
|
||||
SurfaceFactory.instance.releaseSurface(overlay);
|
||||
final RenderCanvas overlay = _overlays[viewId]!;
|
||||
RenderCanvasFactory.instance.releaseCanvas(overlay);
|
||||
_overlays.remove(viewId);
|
||||
}
|
||||
}
|
||||
@ -591,13 +550,13 @@ class HtmlViewEmbedder {
|
||||
// composition order of the current and previous frame, respectively.
|
||||
//
|
||||
// TODO(hterkelsen): Test this more thoroughly.
|
||||
void _updateOverlays(ViewListDiffResult? diffResult) {
|
||||
List<OverlayGroup>? _updateOverlays(ViewListDiffResult? diffResult) {
|
||||
if (diffResult != null &&
|
||||
diffResult.viewsToAdd.isEmpty &&
|
||||
diffResult.viewsToRemove.isEmpty) {
|
||||
// The composition order has not changed, continue using the assigned
|
||||
// overlays.
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
// Group platform views from their composition order.
|
||||
// Each group contains one visible view, and any number of invisible views
|
||||
@ -606,17 +565,10 @@ class HtmlViewEmbedder {
|
||||
getOverlayGroups(_compositionOrder);
|
||||
final List<int> viewsNeedingOverlays =
|
||||
overlayGroups.map((OverlayGroup group) => group.last).toList();
|
||||
// If there were more visible views than overlays, then the last group
|
||||
// doesn't have an overlay.
|
||||
if (viewsNeedingOverlays.length > SurfaceFactory.instance.maximumOverlays) {
|
||||
assert(viewsNeedingOverlays.length ==
|
||||
SurfaceFactory.instance.maximumOverlays + 1);
|
||||
viewsNeedingOverlays.removeLast();
|
||||
}
|
||||
if (diffResult == null) {
|
||||
// Everything is going to be explicitly recomposited anyway. Release all
|
||||
// the surfaces and assign an overlay to all the surfaces needing one.
|
||||
SurfaceFactory.instance.releaseSurfaces();
|
||||
RenderCanvasFactory.instance.releaseCanvases();
|
||||
_overlays.clear();
|
||||
viewsNeedingOverlays.forEach(_initializeOverlay);
|
||||
} else {
|
||||
@ -635,6 +587,7 @@ class HtmlViewEmbedder {
|
||||
.forEach(_initializeOverlay);
|
||||
}
|
||||
assert(_overlays.length == viewsNeedingOverlays.length);
|
||||
return overlayGroups;
|
||||
}
|
||||
|
||||
// Group the platform views into "overlay groups". These are sublists
|
||||
@ -646,12 +599,8 @@ class HtmlViewEmbedder {
|
||||
// be assigned an overlay are grouped together and will be rendered on top of
|
||||
// the rest of the scene.
|
||||
List<OverlayGroup> getOverlayGroups(List<int> views) {
|
||||
final int maxOverlays = SurfaceFactory.instance.maximumOverlays;
|
||||
if (maxOverlays == 0) {
|
||||
return const <OverlayGroup>[];
|
||||
}
|
||||
final List<OverlayGroup> result = <OverlayGroup>[];
|
||||
OverlayGroup currentGroup = OverlayGroup(<int>[]);
|
||||
OverlayGroup currentGroup = OverlayGroup();
|
||||
|
||||
for (int i = 0; i < views.length; i++) {
|
||||
final int view = views[i];
|
||||
@ -660,8 +609,10 @@ class HtmlViewEmbedder {
|
||||
currentGroup.add(view);
|
||||
} else {
|
||||
// `view` is visible.
|
||||
if (!currentGroup.hasVisibleView) {
|
||||
// If `view` is the first visible one of the group, add it.
|
||||
if (!currentGroup.hasVisibleView ||
|
||||
result.length + 1 >= HtmlViewEmbedder.maximumOverlays) {
|
||||
// If `view` is the first visible one of the group or we've reached
|
||||
// the maximum number of overlays, add it.
|
||||
currentGroup.add(view, visible: true);
|
||||
} else {
|
||||
// There's already a visible `view` in `currentGroup`, so a new
|
||||
@ -671,17 +622,8 @@ class HtmlViewEmbedder {
|
||||
// We only care about groups that have one visible view.
|
||||
result.add(currentGroup);
|
||||
}
|
||||
// If there are overlays still available.
|
||||
if (result.length < maxOverlays) {
|
||||
// Create a new group, starting with `view`.
|
||||
currentGroup = OverlayGroup(<int>[view], visible: true);
|
||||
} else {
|
||||
// Add the rest of the views to a final group that will be rendered
|
||||
// on top of the scene.
|
||||
currentGroup = OverlayGroup(views.sublist(i), visible: true);
|
||||
// And break out of the loop!
|
||||
break;
|
||||
}
|
||||
currentGroup = OverlayGroup();
|
||||
currentGroup.add(view, visible: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -696,8 +638,7 @@ class HtmlViewEmbedder {
|
||||
assert(!_overlays.containsKey(viewId));
|
||||
|
||||
// Try reusing a cached overlay created for another platform view.
|
||||
final Surface overlay = SurfaceFactory.instance.getSurface()!;
|
||||
overlay.createOrUpdateSurface(_frameSize);
|
||||
final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas();
|
||||
_overlays[viewId] = overlay;
|
||||
}
|
||||
|
||||
@ -742,29 +683,30 @@ class HtmlViewEmbedder {
|
||||
/// Every overlay group is a list containing a visible view preceded or followed
|
||||
/// by zero or more invisible views.
|
||||
class OverlayGroup {
|
||||
/// Constructor
|
||||
OverlayGroup(
|
||||
List<int> viewGroup, {
|
||||
bool visible = false,
|
||||
}) : _group = viewGroup,
|
||||
_containsVisibleView = visible;
|
||||
OverlayGroup() : _group = <int>[];
|
||||
|
||||
// The internal list of ints.
|
||||
final List<int> _group;
|
||||
// A boolean flag to mark if any visible view has been added to the list.
|
||||
bool _containsVisibleView;
|
||||
|
||||
/// The number of visible views in this group.
|
||||
int _visibleCount = 0;
|
||||
|
||||
/// Add a [view] (maybe [visible]) to this group.
|
||||
void add(int view, {bool visible = false}) {
|
||||
_group.add(view);
|
||||
_containsVisibleView |= visible;
|
||||
if (visible) {
|
||||
_visibleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the "last" view added to this group.
|
||||
int get last => _group.last;
|
||||
|
||||
/// Returns true if this group contains any visible view.
|
||||
bool get hasVisibleView => _group.isNotEmpty && _containsVisibleView;
|
||||
bool get hasVisibleView => _visibleCount > 0;
|
||||
|
||||
/// Returns the number of visible views in this overlay group.
|
||||
int get visibleCount => _visibleCount;
|
||||
}
|
||||
|
||||
/// Represents a Clip Chain (for a view).
|
||||
|
||||
@ -11,8 +11,8 @@ import 'canvas.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
import 'image.dart';
|
||||
import 'native_memory.dart';
|
||||
import 'render_canvas_factory.dart';
|
||||
import 'surface.dart';
|
||||
import 'surface_factory.dart';
|
||||
|
||||
/// Implements [ui.Picture] on top of [SkPicture].
|
||||
class CkPicture implements ScenePicture {
|
||||
@ -99,7 +99,7 @@ class CkPicture implements ScenePicture {
|
||||
CkImage toImageSync(int width, int height) {
|
||||
assert(debugCheckNotDisposed('Cannot convert picture to image.'));
|
||||
|
||||
final Surface surface = SurfaceFactory.instance.pictureToImageSurface;
|
||||
final Surface surface = RenderCanvasFactory.instance.pictureToImageSurface;
|
||||
final CkSurface ckSurface = surface
|
||||
.createOrUpdateSurface(ui.Size(width.toDouble(), height.toDouble()));
|
||||
final CkCanvas ckCanvas = ckSurface.getCanvas();
|
||||
|
||||
@ -3,22 +3,29 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../frame_reference.dart';
|
||||
import 'canvas.dart';
|
||||
import 'embedded_views.dart';
|
||||
import 'layer_tree.dart';
|
||||
import 'surface.dart';
|
||||
import 'surface_factory.dart';
|
||||
|
||||
/// A class that can rasterize [LayerTree]s into a given [Surface].
|
||||
class Rasterizer {
|
||||
final CompositorContext context = CompositorContext();
|
||||
final List<ui.VoidCallback> _postFrameCallbacks = <ui.VoidCallback>[];
|
||||
|
||||
/// This is an SkSurface backed by an OffScreenCanvas. This single Surface is
|
||||
/// used to render to many RenderCanvases to produce the rendered scene.
|
||||
final Surface _offscreenSurface = Surface();
|
||||
ui.Size _currentFrameSize = ui.Size.zero;
|
||||
|
||||
/// Render the given [pictures] so it is displayed by the given [canvas].
|
||||
Future<void> rasterizeToCanvas(
|
||||
RenderCanvas canvas, List<CkPicture> pictures) async {
|
||||
await _offscreenSurface.rasterizeToCanvas(
|
||||
_currentFrameSize, canvas, pictures);
|
||||
}
|
||||
|
||||
/// Sets the maximum size of the Skia resource cache, in bytes.
|
||||
void setSkiaResourceCacheMaxBytes(int bytes) =>
|
||||
SurfaceFactory.instance.baseSurface.setSkiaResourceCacheMaxBytes(bytes);
|
||||
_offscreenSurface.setSkiaResourceCacheMaxBytes(bytes);
|
||||
|
||||
/// Creates a new frame from this rasterizer's surface, draws the given
|
||||
/// [LayerTree] into it, and then submits the frame.
|
||||
@ -29,17 +36,22 @@ class Rasterizer {
|
||||
return;
|
||||
}
|
||||
|
||||
final SurfaceFrame frame =
|
||||
SurfaceFactory.instance.baseSurface.acquireFrame(layerTree.frameSize);
|
||||
HtmlViewEmbedder.instance.frameSize = layerTree.frameSize;
|
||||
final CkCanvas canvas = frame.skiaCanvas;
|
||||
canvas.clear(const ui.Color(0x00000000));
|
||||
final Frame compositorFrame =
|
||||
context.acquireFrame(canvas, HtmlViewEmbedder.instance);
|
||||
_currentFrameSize = layerTree.frameSize;
|
||||
_offscreenSurface.acquireFrame(_currentFrameSize);
|
||||
HtmlViewEmbedder.instance.frameSize = _currentFrameSize;
|
||||
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
|
||||
pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize);
|
||||
pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000));
|
||||
final Frame compositorFrame = context.acquireFrame(
|
||||
pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance);
|
||||
|
||||
compositorFrame.raster(layerTree, ignoreRasterCache: true);
|
||||
SurfaceFactory.instance.baseSurface.addToScene();
|
||||
frame.submit();
|
||||
|
||||
CanvasKitRenderer.instance.sceneHost!
|
||||
.prepend(RenderCanvasFactory.instance.baseCanvas.htmlElement);
|
||||
rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas,
|
||||
<CkPicture>[pictureRecorder.endRecording()]);
|
||||
|
||||
HtmlViewEmbedder.instance.submitFrame();
|
||||
} finally {
|
||||
_runPostFrameCallbacks();
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
// 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:js_interop';
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../dom.dart';
|
||||
import '../window.dart';
|
||||
|
||||
/// A visible (on-screen) canvas that can display bitmaps produced by CanvasKit
|
||||
/// in the (off-screen) SkSurface which is backed by an OffscreenCanvas.
|
||||
///
|
||||
/// In a typical frame, the content will be rendered via CanvasKit in an
|
||||
/// OffscreenCanvas, and then the contents will be transferred to the
|
||||
/// RenderCanvas via `transferFromImageBitmap()`.
|
||||
///
|
||||
/// If we need more RenderCanvases, for example in the case where there are
|
||||
/// platform views and we need overlays to render the frame correctly, then
|
||||
/// we will create multiple RenderCanvases, but crucially still only have
|
||||
/// one OffscreenCanvas which transfers bitmaps to all of the RenderCanvases.
|
||||
///
|
||||
/// To render into the OffscreenCanvas with CanvasKit we need to create a
|
||||
/// WebGL context, which is not only expensive, but the browser has a limit
|
||||
/// on the maximum amount of WebGL contexts which can be live at once. Using
|
||||
/// a single OffscreenCanvas and multiple RenderCanvases allows us to only
|
||||
/// create a single WebGL context.
|
||||
class RenderCanvas {
|
||||
RenderCanvas() {
|
||||
canvasElement.setAttribute('aria-hidden', 'true');
|
||||
canvasElement.style.position = 'absolute';
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
htmlElement.append(canvasElement);
|
||||
}
|
||||
|
||||
/// The root HTML element for this canvas.
|
||||
///
|
||||
/// This element contains the canvas used to draw the UI. Unlike the canvas,
|
||||
/// this element is permanent. It is never replaced or deleted, until this
|
||||
/// canvas is disposed of via [dispose].
|
||||
///
|
||||
/// Conversely, the canvas that lives inside this element can be swapped, for
|
||||
/// example, when the screen size changes, or when the WebGL context is lost
|
||||
/// due to the browser tab becoming dormant.
|
||||
final DomElement htmlElement = createDomElement('flt-canvas-container');
|
||||
|
||||
/// The underlying `<canvas>` element used to display the pixels.
|
||||
final DomCanvasElement canvasElement = createDomCanvasElement();
|
||||
int _pixelWidth = 0;
|
||||
int _pixelHeight = 0;
|
||||
|
||||
late final DomCanvasRenderingContextBitmapRenderer renderContext =
|
||||
canvasElement.contextBitmapRenderer;
|
||||
|
||||
double _currentDevicePixelRatio = -1;
|
||||
|
||||
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
|
||||
/// pixels.
|
||||
///
|
||||
/// The logical size of the canvas is not based on the size of the window
|
||||
/// but on the size of the canvas, which, due to `ceil()` above, may not be
|
||||
/// the same as the window. We do not round/floor/ceil the logical size as
|
||||
/// CSS pixels can contain more than one physical pixel and therefore to
|
||||
/// match the size of the window precisely we use the most precise floating
|
||||
/// point value we can get.
|
||||
void _updateLogicalHtmlCanvasSize() {
|
||||
final double logicalWidth = _pixelWidth / window.devicePixelRatio;
|
||||
final double logicalHeight = _pixelHeight / window.devicePixelRatio;
|
||||
final DomCSSStyleDeclaration style = canvasElement.style;
|
||||
style.width = '${logicalWidth}px';
|
||||
style.height = '${logicalHeight}px';
|
||||
_currentDevicePixelRatio = window.devicePixelRatio;
|
||||
}
|
||||
|
||||
/// Render the given [bitmap] with this [RenderCanvas].
|
||||
///
|
||||
/// The canvas will be resized to accomodate the bitmap immediately before
|
||||
/// rendering it.
|
||||
void render(DomImageBitmap bitmap) {
|
||||
_ensureSize(ui.Size(bitmap.width.toDartDouble, bitmap.height.toDartDouble));
|
||||
renderContext.transferFromImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
/// Ensures that this canvas can draw a frame of the given [size].
|
||||
void _ensureSize(ui.Size size) {
|
||||
// Check if the frame is the same size as before, and if so, we don't need
|
||||
// to resize the canvas.
|
||||
if (size.width.ceil() == _pixelWidth &&
|
||||
size.height.ceil() == _pixelHeight) {
|
||||
// The existing canvas doesn't need to be resized (unless the device pixel
|
||||
// ratio changed).
|
||||
if (window.devicePixelRatio != _currentDevicePixelRatio) {
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the canvas is too large or too small, resize it to the exact size of
|
||||
// the frame. We cannot allow the canvas to be larger than the screen
|
||||
// because then when we call `transferFromImageBitmap()` the bitmap will
|
||||
// be scaled to cover the entire canvas.
|
||||
_pixelWidth = size.width.ceil();
|
||||
_pixelHeight = size.height.ceil();
|
||||
canvasElement.width = _pixelWidth.toDouble();
|
||||
canvasElement.height = _pixelHeight.toDouble();
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
htmlElement.remove();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
// 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:meta/meta.dart';
|
||||
|
||||
import '../../engine.dart';
|
||||
|
||||
/// Caches canvases used to overlay platform views.
|
||||
class RenderCanvasFactory {
|
||||
RenderCanvasFactory() {
|
||||
assert(() {
|
||||
registerHotRestartListener(debugClear);
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
/// The lazy-initialized singleton surface factory.
|
||||
///
|
||||
/// [debugClear] causes this singleton to be reinitialized.
|
||||
static RenderCanvasFactory get instance =>
|
||||
_instance ??= RenderCanvasFactory();
|
||||
|
||||
/// Returns the raw (potentially uninitialized) value of the singleton.
|
||||
///
|
||||
/// Useful in tests for checking the lifecycle of this class.
|
||||
static RenderCanvasFactory? get debugUninitializedInstance => _instance;
|
||||
|
||||
// Override the current instance with a new one.
|
||||
//
|
||||
// This should only be used in tests.
|
||||
static void debugSetInstance(RenderCanvasFactory newInstance) {
|
||||
_instance = newInstance;
|
||||
}
|
||||
|
||||
static RenderCanvasFactory? _instance;
|
||||
|
||||
/// The base canvas to paint on. This is the default canvas which will be
|
||||
/// painted to. If there are no platform views, then this canvas will render
|
||||
/// the entire scene.
|
||||
final RenderCanvas baseCanvas = RenderCanvas();
|
||||
|
||||
/// A surface used specifically for `Picture.toImage` when software rendering
|
||||
/// is supported.
|
||||
late final Surface pictureToImageSurface = Surface();
|
||||
|
||||
/// Canvases created by this factory which are currently in use.
|
||||
final List<RenderCanvas> _liveCanvases = <RenderCanvas>[];
|
||||
|
||||
/// Canvases created by this factory which are no longer in use. These can be
|
||||
/// reused.
|
||||
final List<RenderCanvas> _cache = <RenderCanvas>[];
|
||||
|
||||
/// The number of canvases which have been created by this factory.
|
||||
int get _canvasCount => _liveCanvases.length + _cache.length + 1;
|
||||
|
||||
/// The number of surfaces created by this factory. Used for testing.
|
||||
@visibleForTesting
|
||||
int get debugSurfaceCount => _canvasCount;
|
||||
|
||||
/// Returns the number of cached surfaces.
|
||||
///
|
||||
/// Useful in tests.
|
||||
int get debugCacheSize => _cache.length;
|
||||
|
||||
/// Gets an overlay canvas from the cache or creates a new one if there are
|
||||
/// none in the cache.
|
||||
RenderCanvas getCanvas() {
|
||||
if (_cache.isNotEmpty) {
|
||||
final RenderCanvas canvas = _cache.removeLast();
|
||||
_liveCanvases.add(canvas);
|
||||
return canvas;
|
||||
} else {
|
||||
final RenderCanvas canvas = RenderCanvas();
|
||||
_liveCanvases.add(canvas);
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
/// Releases all surfaces so they can be reused in the next frame.
|
||||
///
|
||||
/// If a released surface is in the DOM, it is not removed. This allows the
|
||||
/// engine to release the surfaces at the end of the frame so they are ready
|
||||
/// to be used in the next frame, but still used for painting in the current
|
||||
/// frame.
|
||||
void releaseCanvases() {
|
||||
_cache.addAll(_liveCanvases);
|
||||
_liveCanvases.clear();
|
||||
}
|
||||
|
||||
/// Removes all surfaces except the base surface from the DOM.
|
||||
///
|
||||
/// This is called at the beginning of the frame to prepare for painting into
|
||||
/// the new surfaces.
|
||||
void removeSurfacesFromDom() {
|
||||
_cache.forEach(_removeFromDom);
|
||||
}
|
||||
|
||||
// Removes [canvas] from the DOM.
|
||||
void _removeFromDom(RenderCanvas canvas) {
|
||||
canvas.htmlElement.remove();
|
||||
}
|
||||
|
||||
/// Signals that a canvas is no longer being used. It can be reused.
|
||||
void releaseCanvas(RenderCanvas canvas) {
|
||||
assert(canvas != baseCanvas, 'Attempting to release the base canvas');
|
||||
assert(
|
||||
_liveCanvases.contains(canvas),
|
||||
'Attempting to release a Canvas which '
|
||||
'was not created by this factory');
|
||||
canvas.htmlElement.remove();
|
||||
_liveCanvases.remove(canvas);
|
||||
_cache.add(canvas);
|
||||
}
|
||||
|
||||
/// Returns [true] if [canvas] is currently being used to paint content.
|
||||
///
|
||||
/// The base canvas always counts as live.
|
||||
///
|
||||
/// If a canvas is not live, then it must be in the cache and ready to be
|
||||
/// reused.
|
||||
bool isLive(RenderCanvas canvas) {
|
||||
if (canvas == baseCanvas || _liveCanvases.contains(canvas)) {
|
||||
return true;
|
||||
}
|
||||
assert(_cache.contains(canvas));
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Dispose all canvases created by this factory. Used in tests.
|
||||
void debugClear() {
|
||||
for (final RenderCanvas canvas in _cache) {
|
||||
canvas.dispose();
|
||||
}
|
||||
for (final RenderCanvas canvas in _liveCanvases) {
|
||||
canvas.dispose();
|
||||
}
|
||||
baseCanvas.dispose();
|
||||
_liveCanvases.clear();
|
||||
_cache.clear();
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
@ -11,11 +11,10 @@ import '../configuration.dart';
|
||||
import '../dom.dart';
|
||||
import '../platform_dispatcher.dart';
|
||||
import '../util.dart';
|
||||
import '../window.dart';
|
||||
import 'canvas.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
import 'renderer.dart';
|
||||
import 'surface_factory.dart';
|
||||
import 'picture.dart';
|
||||
import 'render_canvas.dart';
|
||||
import 'util.dart';
|
||||
|
||||
// Only supported in profile/release mode. Allows Flutter to use MSAA but
|
||||
@ -26,8 +25,7 @@ typedef SubmitCallback = bool Function(SurfaceFrame, CkCanvas);
|
||||
|
||||
/// A frame which contains a canvas to be drawn into.
|
||||
class SurfaceFrame {
|
||||
SurfaceFrame(this.skiaSurface, this.submitCallback)
|
||||
: _submitted = false;
|
||||
SurfaceFrame(this.skiaSurface, this.submitCallback) : _submitted = false;
|
||||
|
||||
final CkSurface skiaSurface;
|
||||
final SubmitCallback submitCallback;
|
||||
@ -82,19 +80,16 @@ class Surface {
|
||||
int? _glContext;
|
||||
int? _skiaCacheBytes;
|
||||
|
||||
/// The root HTML element for this surface.
|
||||
///
|
||||
/// This element contains the canvas used to draw the UI. Unlike the canvas,
|
||||
/// this element is permanent. It is never replaced or deleted, until this
|
||||
/// surface is disposed of via [dispose].
|
||||
///
|
||||
/// Conversely, the canvas that lives inside this element can be swapped, for
|
||||
/// example, when the screen size changes, or when the WebGL context is lost
|
||||
/// due to the browser tab becoming dormant.
|
||||
final DomElement htmlElement = createDomElement('flt-canvas-container');
|
||||
/// The underlying OffscreenCanvas element used for this surface.
|
||||
DomOffscreenCanvas? _offscreenCanvas;
|
||||
|
||||
/// Returns the underlying OffscreenCanvas. Should only be used in tests.
|
||||
DomOffscreenCanvas? get debugOffscreenCanvas => _offscreenCanvas;
|
||||
|
||||
/// The <canvas> backing this Surface in the case that OffscreenCanvas isn't
|
||||
/// supported.
|
||||
DomCanvasElement? _canvasElement;
|
||||
|
||||
/// The underlying `<canvas>` element used for this surface.
|
||||
DomCanvasElement? htmlCanvas;
|
||||
int _pixelWidth = -1;
|
||||
int _pixelHeight = -1;
|
||||
int _sampleCount = -1;
|
||||
@ -112,7 +107,31 @@ class Surface {
|
||||
}
|
||||
}
|
||||
|
||||
bool _addedToScene = false;
|
||||
Future<void> rasterizeToCanvas(
|
||||
ui.Size frameSize, RenderCanvas canvas, List<CkPicture> pictures) async {
|
||||
final CkCanvas skCanvas = _surface!.getCanvas();
|
||||
skCanvas.clear(const ui.Color(0x00000000));
|
||||
pictures.forEach(skCanvas.drawPicture);
|
||||
_surface!.flush();
|
||||
|
||||
DomImageBitmap bitmap;
|
||||
if (Surface.offscreenCanvasSupported) {
|
||||
bitmap = (await createImageBitmap(_offscreenCanvas!, (
|
||||
x: 0,
|
||||
y: _pixelHeight - frameSize.height.toInt(),
|
||||
width: frameSize.width.toInt(),
|
||||
height: frameSize.height.toInt(),
|
||||
)).toDart)! as DomImageBitmap;
|
||||
} else {
|
||||
bitmap = (await createImageBitmap(_canvasElement!, (
|
||||
x: 0,
|
||||
y: _pixelHeight - frameSize.height.toInt(),
|
||||
width: frameSize.width.toInt(),
|
||||
height: frameSize.height.toInt()
|
||||
)).toDart)! as DomImageBitmap;
|
||||
}
|
||||
canvas.render(bitmap);
|
||||
}
|
||||
|
||||
/// Acquire a frame of the given [size] containing a drawable canvas.
|
||||
///
|
||||
@ -129,21 +148,16 @@ class Surface {
|
||||
return SurfaceFrame(surface, submitCallback);
|
||||
}
|
||||
|
||||
void addToScene() {
|
||||
if (!_addedToScene) {
|
||||
CanvasKitRenderer.instance.sceneHost!.prepend(htmlElement);
|
||||
}
|
||||
_addedToScene = true;
|
||||
}
|
||||
|
||||
ui.Size? _currentCanvasPhysicalSize;
|
||||
ui.Size? _currentSurfaceSize;
|
||||
double _currentDevicePixelRatio = -1;
|
||||
|
||||
/// This is only valid after the first frame or if [ensureSurface] has been
|
||||
/// called
|
||||
bool get usingSoftwareBackend => _glContext == null ||
|
||||
_grContext == null || webGLVersion == -1 || configuration.canvasKitForceCpuOnly;
|
||||
bool get usingSoftwareBackend =>
|
||||
_glContext == null ||
|
||||
_grContext == null ||
|
||||
webGLVersion == -1 ||
|
||||
configuration.canvasKitForceCpuOnly;
|
||||
|
||||
/// Ensure that the initial surface exists and has a size of at least [size].
|
||||
///
|
||||
@ -159,22 +173,10 @@ class Surface {
|
||||
}
|
||||
// TODO(jonahwilliams): this is somewhat wasteful. We should probably
|
||||
// eagerly setup this surface instead of delaying until the first frame?
|
||||
// Or at least cache the estimated window size.
|
||||
// Or at least cache the estimated window sizeThis is the first frame we have rendered with this canvas.
|
||||
createOrUpdateSurface(size);
|
||||
}
|
||||
|
||||
/// This method is not supported if software rendering is used.
|
||||
CkSurface createRenderTargetSurface(ui.Size size) {
|
||||
assert(!usingSoftwareBackend);
|
||||
|
||||
final SkSurface skSurface = canvasKit.MakeRenderTarget(
|
||||
_grContext!,
|
||||
size.width.ceil(),
|
||||
size.height.ceil(),
|
||||
)!;
|
||||
return CkSurface(skSurface, _glContext);
|
||||
}
|
||||
|
||||
/// Creates a <canvas> and SkSurface for the given [size].
|
||||
CkSurface createOrUpdateSurface(ui.Size size) {
|
||||
if (size.isEmpty) {
|
||||
@ -188,11 +190,6 @@ class Surface {
|
||||
if (previousSurfaceSize != null &&
|
||||
size.width == previousSurfaceSize.width &&
|
||||
size.height == previousSurfaceSize.height) {
|
||||
// The existing surface is still reusable.
|
||||
if (window.devicePixelRatio != _currentDevicePixelRatio) {
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
_translateCanvas();
|
||||
}
|
||||
return _surface!;
|
||||
}
|
||||
|
||||
@ -205,12 +202,16 @@ class Surface {
|
||||
final ui.Size newSize = size * 1.4;
|
||||
_surface?.dispose();
|
||||
_surface = null;
|
||||
htmlCanvas!.width = newSize.width;
|
||||
htmlCanvas!.height = newSize.height;
|
||||
if (Surface.offscreenCanvasSupported) {
|
||||
_offscreenCanvas!.width = newSize.width;
|
||||
_offscreenCanvas!.height = newSize.height;
|
||||
} else {
|
||||
_canvasElement!.width = newSize.width;
|
||||
_canvasElement!.height = newSize.height;
|
||||
}
|
||||
_currentCanvasPhysicalSize = newSize;
|
||||
_pixelWidth = newSize.width.ceil();
|
||||
_pixelHeight = newSize.height.ceil();
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,57 +219,20 @@ class Surface {
|
||||
if (_forceNewContext || _currentCanvasPhysicalSize == null) {
|
||||
_surface?.dispose();
|
||||
_surface = null;
|
||||
_addedToScene = false;
|
||||
_grContext?.releaseResourcesAndAbandonContext();
|
||||
_grContext?.delete();
|
||||
_grContext = null;
|
||||
|
||||
_createNewCanvas(size);
|
||||
_currentCanvasPhysicalSize = size;
|
||||
} else if (window.devicePixelRatio != _currentDevicePixelRatio) {
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
|
||||
_currentDevicePixelRatio = window.devicePixelRatio;
|
||||
_currentSurfaceSize = size;
|
||||
_translateCanvas();
|
||||
_surface?.dispose();
|
||||
_surface = _createNewSurface(size);
|
||||
return _surface!;
|
||||
}
|
||||
|
||||
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
|
||||
/// pixels.
|
||||
///
|
||||
/// The logical size of the canvas is not based on the size of the window
|
||||
/// but on the size of the canvas, which, due to `ceil()` above, may not be
|
||||
/// the same as the window. We do not round/floor/ceil the logical size as
|
||||
/// CSS pixels can contain more than one physical pixel and therefore to
|
||||
/// match the size of the window precisely we use the most precise floating
|
||||
/// point value we can get.
|
||||
void _updateLogicalHtmlCanvasSize() {
|
||||
final double logicalWidth = _pixelWidth / window.devicePixelRatio;
|
||||
final double logicalHeight = _pixelHeight / window.devicePixelRatio;
|
||||
final DomCSSStyleDeclaration style = htmlCanvas!.style;
|
||||
style.width = '${logicalWidth}px';
|
||||
style.height = '${logicalHeight}px';
|
||||
}
|
||||
|
||||
/// Translate the canvas so the surface covers the visible portion of the
|
||||
/// screen.
|
||||
///
|
||||
/// The <canvas> may be larger than the visible screen, but the SkSurface is
|
||||
/// exactly the size of the visible screen. Unfortunately, the SkSurface is
|
||||
/// drawn in the lower left corner of the <canvas>, and without translation,
|
||||
/// only the top left of the <canvas> is visible. So we shift the canvas up so
|
||||
/// the bottom left corner is visible.
|
||||
void _translateCanvas() {
|
||||
final int surfaceHeight = _currentSurfaceSize!.height.ceil();
|
||||
final double offset =
|
||||
(_pixelHeight - surfaceHeight) / window.devicePixelRatio;
|
||||
htmlCanvas!.style.transform = 'translate(0, -${offset}px)';
|
||||
}
|
||||
|
||||
JSVoid _contextRestoredListener(DomEvent event) {
|
||||
assert(
|
||||
_contextLost,
|
||||
@ -282,16 +246,11 @@ class Surface {
|
||||
}
|
||||
|
||||
JSVoid _contextLostListener(DomEvent event) {
|
||||
assert(event.target == htmlCanvas,
|
||||
assert(event.target == _offscreenCanvas || event.target == _canvasElement,
|
||||
'Received a context lost event for a disposed canvas');
|
||||
final SurfaceFactory factory = SurfaceFactory.instance;
|
||||
_contextLost = true;
|
||||
if (factory.isLive(this)) {
|
||||
_forceNewContext = true;
|
||||
event.preventDefault();
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
_forceNewContext = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/// This function is expensive.
|
||||
@ -299,18 +258,32 @@ class Surface {
|
||||
/// It's better to reuse canvas if possible.
|
||||
void _createNewCanvas(ui.Size physicalSize) {
|
||||
// Clear the container, if it's not empty. We're going to create a new <canvas>.
|
||||
if (this.htmlCanvas != null) {
|
||||
this.htmlCanvas!.removeEventListener(
|
||||
'webglcontextrestored',
|
||||
_cachedContextRestoredListener,
|
||||
false,
|
||||
);
|
||||
this.htmlCanvas!.removeEventListener(
|
||||
'webglcontextlost',
|
||||
_cachedContextLostListener,
|
||||
false,
|
||||
);
|
||||
this.htmlCanvas!.remove();
|
||||
if (_offscreenCanvas != null) {
|
||||
_offscreenCanvas!.removeEventListener(
|
||||
'webglcontextrestored',
|
||||
_cachedContextRestoredListener,
|
||||
false,
|
||||
);
|
||||
_offscreenCanvas!.removeEventListener(
|
||||
'webglcontextlost',
|
||||
_cachedContextLostListener,
|
||||
false,
|
||||
);
|
||||
_offscreenCanvas = null;
|
||||
_cachedContextRestoredListener = null;
|
||||
_cachedContextLostListener = null;
|
||||
} else if (_canvasElement != null) {
|
||||
_canvasElement!.removeEventListener(
|
||||
'webglcontextrestored',
|
||||
_cachedContextRestoredListener,
|
||||
false,
|
||||
);
|
||||
_canvasElement!.removeEventListener(
|
||||
'webglcontextlost',
|
||||
_cachedContextLostListener,
|
||||
false,
|
||||
);
|
||||
_canvasElement = null;
|
||||
_cachedContextRestoredListener = null;
|
||||
_cachedContextLostListener = null;
|
||||
}
|
||||
@ -319,25 +292,22 @@ class Surface {
|
||||
// we ensure that the rendred picture covers the entire browser window.
|
||||
_pixelWidth = physicalSize.width.ceil();
|
||||
_pixelHeight = physicalSize.height.ceil();
|
||||
final DomCanvasElement htmlCanvas = createDomCanvasElement(
|
||||
width: _pixelWidth,
|
||||
height: _pixelHeight,
|
||||
);
|
||||
this.htmlCanvas = htmlCanvas;
|
||||
|
||||
// The DOM elements used to render pictures are used purely to put pixels on
|
||||
// the screen. They have no semantic information. If an assistive technology
|
||||
// attempts to scan picture content it will look like garbage and confuse
|
||||
// users. UI semantics are exported as a separate DOM tree rendered parallel
|
||||
// to pictures.
|
||||
//
|
||||
// Why are layer and scene elements not hidden from ARIA? Because those
|
||||
// elements may contain platform views, and platform views must be
|
||||
// accessible.
|
||||
htmlCanvas.setAttribute('aria-hidden', 'true');
|
||||
|
||||
htmlCanvas.style.position = 'absolute';
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
DomEventTarget htmlCanvas;
|
||||
if (Surface.offscreenCanvasSupported) {
|
||||
final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(
|
||||
_pixelWidth,
|
||||
_pixelHeight,
|
||||
);
|
||||
htmlCanvas = offscreenCanvas;
|
||||
_offscreenCanvas = offscreenCanvas;
|
||||
_canvasElement = null;
|
||||
} else {
|
||||
final DomCanvasElement canvas =
|
||||
createDomCanvasElement(width: _pixelWidth, height: _pixelHeight);
|
||||
htmlCanvas = canvas;
|
||||
_canvasElement = canvas;
|
||||
_offscreenCanvas = null;
|
||||
}
|
||||
|
||||
// When the browser tab using WebGL goes dormant the browser and/or OS may
|
||||
// decide to clear GPU resources to let other tabs/programs use the GPU.
|
||||
@ -345,7 +315,8 @@ class Surface {
|
||||
// notification. When we receive this notification we force a new context.
|
||||
//
|
||||
// See also: https://www.khronos.org/webgl/wiki/HandlingContextLost
|
||||
_cachedContextRestoredListener = createDomEventListener(_contextRestoredListener);
|
||||
_cachedContextRestoredListener =
|
||||
createDomEventListener(_contextRestoredListener);
|
||||
_cachedContextLostListener = createDomEventListener(_contextLostListener);
|
||||
htmlCanvas.addEventListener(
|
||||
'webglcontextlost',
|
||||
@ -361,15 +332,24 @@ class Surface {
|
||||
_contextLost = false;
|
||||
|
||||
if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) {
|
||||
final int glContext = canvasKit.GetWebGLContext(
|
||||
htmlCanvas,
|
||||
SkWebGLContextOptions(
|
||||
// Default to no anti-aliasing. Paint commands can be explicitly
|
||||
// anti-aliased by setting their `Paint` object's `antialias` property.
|
||||
antialias: _kUsingMSAA ? 1 : 0,
|
||||
majorVersion: webGLVersion.toDouble(),
|
||||
),
|
||||
).toInt();
|
||||
int glContext = 0;
|
||||
final SkWebGLContextOptions options = SkWebGLContextOptions(
|
||||
// Default to no anti-aliasing. Paint commands can be explicitly
|
||||
// anti-aliased by setting their `Paint` object's `antialias` property.
|
||||
antialias: _kUsingMSAA ? 1 : 0,
|
||||
majorVersion: webGLVersion.toDouble(),
|
||||
);
|
||||
if (Surface.offscreenCanvasSupported) {
|
||||
glContext = canvasKit.GetOffscreenWebGLContext(
|
||||
_offscreenCanvas!,
|
||||
options,
|
||||
).toInt();
|
||||
} else {
|
||||
glContext = canvasKit.GetWebGLContext(
|
||||
_canvasElement!,
|
||||
options,
|
||||
).toInt();
|
||||
}
|
||||
|
||||
_glContext = glContext;
|
||||
|
||||
@ -387,40 +367,38 @@ class Surface {
|
||||
_syncCacheBytes();
|
||||
}
|
||||
}
|
||||
|
||||
htmlElement.append(htmlCanvas);
|
||||
}
|
||||
|
||||
void _initWebglParams() {
|
||||
final WebGLContext gl = htmlCanvas!.getGlContext(webGLVersion);
|
||||
WebGLContext gl;
|
||||
if (Surface.offscreenCanvasSupported) {
|
||||
gl = _offscreenCanvas!.getGlContext(webGLVersion);
|
||||
} else {
|
||||
gl = _canvasElement!.getGlContext(webGLVersion);
|
||||
}
|
||||
_sampleCount = gl.getParameter(gl.samples);
|
||||
_stencilBits = gl.getParameter(gl.stencilBits);
|
||||
}
|
||||
|
||||
CkSurface _createNewSurface(ui.Size size) {
|
||||
assert(htmlCanvas != null);
|
||||
assert(_offscreenCanvas != null || _canvasElement != null);
|
||||
if (webGLVersion == -1) {
|
||||
return _makeSoftwareCanvasSurface(
|
||||
htmlCanvas!, 'WebGL support not detected');
|
||||
return _makeSoftwareCanvasSurface('WebGL support not detected');
|
||||
} else if (configuration.canvasKitForceCpuOnly) {
|
||||
return _makeSoftwareCanvasSurface(
|
||||
htmlCanvas!, 'CPU rendering forced by application');
|
||||
return _makeSoftwareCanvasSurface('CPU rendering forced by application');
|
||||
} else if (_glContext == 0) {
|
||||
return _makeSoftwareCanvasSurface(
|
||||
htmlCanvas!, 'Failed to initialize WebGL context');
|
||||
return _makeSoftwareCanvasSurface('Failed to initialize WebGL context');
|
||||
} else {
|
||||
final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface(
|
||||
_grContext!,
|
||||
size.width.roundToDouble(),
|
||||
size.height.roundToDouble(),
|
||||
SkColorSpaceSRGB,
|
||||
_sampleCount,
|
||||
_stencilBits
|
||||
);
|
||||
_grContext!,
|
||||
size.width.roundToDouble(),
|
||||
size.height.roundToDouble(),
|
||||
SkColorSpaceSRGB,
|
||||
_sampleCount,
|
||||
_stencilBits);
|
||||
|
||||
if (skSurface == null) {
|
||||
return _makeSoftwareCanvasSurface(
|
||||
htmlCanvas!, 'Failed to initialize WebGL surface');
|
||||
return _makeSoftwareCanvasSurface('Failed to initialize WebGL surface');
|
||||
}
|
||||
|
||||
return CkSurface(skSurface, _glContext);
|
||||
@ -429,14 +407,20 @@ class Surface {
|
||||
|
||||
static bool _didWarnAboutWebGlInitializationFailure = false;
|
||||
|
||||
CkSurface _makeSoftwareCanvasSurface(
|
||||
DomCanvasElement htmlCanvas, String reason) {
|
||||
CkSurface _makeSoftwareCanvasSurface(String reason) {
|
||||
if (!_didWarnAboutWebGlInitializationFailure) {
|
||||
printWarning('WARNING: Falling back to CPU-only rendering. $reason.');
|
||||
_didWarnAboutWebGlInitializationFailure = true;
|
||||
}
|
||||
|
||||
SkSurface surface;
|
||||
if (Surface.offscreenCanvasSupported) {
|
||||
surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!);
|
||||
} else {
|
||||
surface = canvasKit.MakeSWCanvasSurface(_canvasElement!);
|
||||
}
|
||||
return CkSurface(
|
||||
canvasKit.MakeSWCanvasSurface(htmlCanvas),
|
||||
surface,
|
||||
null,
|
||||
);
|
||||
}
|
||||
@ -447,15 +431,19 @@ class Surface {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
htmlCanvas?.removeEventListener(
|
||||
_offscreenCanvas?.removeEventListener(
|
||||
'webglcontextlost', _cachedContextLostListener, false);
|
||||
htmlCanvas?.removeEventListener(
|
||||
_offscreenCanvas?.removeEventListener(
|
||||
'webglcontextrestored', _cachedContextRestoredListener, false);
|
||||
_cachedContextLostListener = null;
|
||||
_cachedContextRestoredListener = null;
|
||||
htmlElement.remove();
|
||||
_surface?.dispose();
|
||||
}
|
||||
|
||||
/// Safari 15 doesn't support OffscreenCanvas at all. Safari 16 supports
|
||||
/// OffscreenCanvas, but only with the context2d API, not WebGL.
|
||||
static bool get offscreenCanvasSupported =>
|
||||
browserSupportsOffscreenCanvas && !isSafari;
|
||||
}
|
||||
|
||||
/// A Dart wrapper around Skia's CkSurface.
|
||||
|
||||
@ -1,167 +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 show max;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../engine.dart';
|
||||
|
||||
/// Caches surfaces used to overlay platform views.
|
||||
class SurfaceFactory {
|
||||
SurfaceFactory(int maximumSurfaces)
|
||||
: maximumSurfaces = math.max(maximumSurfaces, 1) {
|
||||
assert(() {
|
||||
if (maximumSurfaces < 1) {
|
||||
printWarning('Attempted to create a $SurfaceFactory with '
|
||||
'$maximumSurfaces maximum surfaces. At least 1 surface is required '
|
||||
'for rendering.');
|
||||
}
|
||||
registerHotRestartListener(debugClear);
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
/// The lazy-initialized singleton surface factory.
|
||||
///
|
||||
/// [debugClear] causes this singleton to be reinitialized.
|
||||
static SurfaceFactory get instance =>
|
||||
_instance ??= SurfaceFactory(configuration.canvasKitMaximumSurfaces);
|
||||
|
||||
/// Returns the raw (potentially uninitialized) value of the singleton.
|
||||
///
|
||||
/// Useful in tests for checking the lifecycle of this class.
|
||||
static SurfaceFactory? get debugUninitializedInstance => _instance;
|
||||
|
||||
// Override the current instance with a new one.
|
||||
//
|
||||
// This should only be used in tests.
|
||||
static void debugSetInstance(SurfaceFactory newInstance) {
|
||||
_instance = newInstance;
|
||||
}
|
||||
|
||||
static SurfaceFactory? _instance;
|
||||
|
||||
/// The base surface to paint on. This is the default surface which will be
|
||||
/// painted to. If there are no platform views, then this surface will receive
|
||||
/// all painting commands.
|
||||
final Surface baseSurface = Surface();
|
||||
|
||||
/// The maximum number of surfaces which can be live at once.
|
||||
final int maximumSurfaces;
|
||||
|
||||
/// A surface used specifically for `Picture.toImage` when software rendering
|
||||
/// is supported.
|
||||
late final Surface pictureToImageSurface = Surface();
|
||||
|
||||
/// The maximum number of assignable overlays.
|
||||
///
|
||||
/// This is just `maximumSurfaces - 1` (the maximum number of surfaces minus
|
||||
/// the required base surface).
|
||||
int get maximumOverlays => maximumSurfaces - 1;
|
||||
|
||||
/// Surfaces created by this factory which are currently in use.
|
||||
final List<Surface> _liveSurfaces = <Surface>[];
|
||||
|
||||
/// Surfaces created by this factory which are no longer in use. These can be
|
||||
/// reused.
|
||||
final List<Surface> _cache = <Surface>[];
|
||||
|
||||
/// The number of surfaces which have been created by this factory.
|
||||
int get _surfaceCount => _liveSurfaces.length + _cache.length + 1;
|
||||
|
||||
/// The number of available overlay surfaces.
|
||||
///
|
||||
/// This does not include the base surface.
|
||||
int get numAvailableOverlays => maximumOverlays - _liveSurfaces.length;
|
||||
|
||||
/// The number of surfaces created by this factory. Used for testing.
|
||||
@visibleForTesting
|
||||
int get debugSurfaceCount => _surfaceCount;
|
||||
|
||||
/// Returns the number of cached surfaces.
|
||||
///
|
||||
/// Useful in tests.
|
||||
int get debugCacheSize => _cache.length;
|
||||
|
||||
/// Gets an overlay surface from the cache or creates a new one if it wouldn't
|
||||
/// exceed the maximum. If there are no available surfaces, returns `null`.
|
||||
Surface? getSurface() {
|
||||
if (_cache.isNotEmpty) {
|
||||
final Surface surface = _cache.removeLast();
|
||||
_liveSurfaces.add(surface);
|
||||
return surface;
|
||||
} else if (debugSurfaceCount < maximumSurfaces) {
|
||||
final Surface surface = Surface();
|
||||
_liveSurfaces.add(surface);
|
||||
return surface;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Releases all surfaces so they can be reused in the next frame.
|
||||
///
|
||||
/// If a released surface is in the DOM, it is not removed. This allows the
|
||||
/// engine to release the surfaces at the end of the frame so they are ready
|
||||
/// to be used in the next frame, but still used for painting in the current
|
||||
/// frame.
|
||||
void releaseSurfaces() {
|
||||
_cache.addAll(_liveSurfaces);
|
||||
_liveSurfaces.clear();
|
||||
}
|
||||
|
||||
/// Removes all surfaces except the base surface from the DOM.
|
||||
///
|
||||
/// This is called at the beginning of the frame to prepare for painting into
|
||||
/// the new surfaces.
|
||||
void removeSurfacesFromDom() {
|
||||
_cache.forEach(_removeFromDom);
|
||||
}
|
||||
|
||||
// Removes [surface] from the DOM.
|
||||
void _removeFromDom(Surface surface) {
|
||||
surface.htmlElement.remove();
|
||||
}
|
||||
|
||||
/// Signals that a surface is no longer being used. It can be reused.
|
||||
void releaseSurface(Surface surface) {
|
||||
assert(surface != baseSurface, 'Attempting to release the base surface');
|
||||
assert(
|
||||
_liveSurfaces.contains(surface),
|
||||
'Attempting to release a Surface which '
|
||||
'was not created by this factory');
|
||||
surface.htmlElement.remove();
|
||||
_liveSurfaces.remove(surface);
|
||||
_cache.add(surface);
|
||||
}
|
||||
|
||||
/// Returns [true] if [surface] is currently being used to paint content.
|
||||
///
|
||||
/// The base surface always counts as live.
|
||||
///
|
||||
/// If a surface is not live, then it must be in the cache and ready to be
|
||||
/// reused.
|
||||
bool isLive(Surface surface) {
|
||||
if (surface == baseSurface ||
|
||||
_liveSurfaces.contains(surface)) {
|
||||
return true;
|
||||
}
|
||||
assert(_cache.contains(surface));
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Dispose all surfaces created by this factory. Used in tests.
|
||||
void debugClear() {
|
||||
for (final Surface surface in _cache) {
|
||||
surface.dispose();
|
||||
}
|
||||
for (final Surface surface in _liveSurfaces) {
|
||||
surface.dispose();
|
||||
}
|
||||
baseSurface.dispose();
|
||||
_liveSurfaces.clear();
|
||||
_cache.clear();
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
@ -257,15 +257,10 @@ class FlutterConfiguration {
|
||||
'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY',
|
||||
);
|
||||
|
||||
/// The maximum number of overlay surfaces that the CanvasKit renderer will use.
|
||||
///
|
||||
/// Overlay surfaces are extra WebGL `<canvas>` elements used to paint on top
|
||||
/// of platform views. Too many platform views can cause the browser to run
|
||||
/// out of resources (memory, CPU, GPU) to handle the content efficiently.
|
||||
/// The number of overlay surfaces is therefore limited.
|
||||
///
|
||||
/// This value can be specified using either the `FLUTTER_WEB_MAXIMUM_SURFACES`
|
||||
/// environment variable, or using the runtime configuration.
|
||||
/// This is deprecated. The CanvasKit renderer will only ever create one
|
||||
/// WebGL context, obviating the problem this configuration was meant to
|
||||
/// solve originally.
|
||||
@Deprecated('Setting canvasKitMaximumSurfaces has no effect')
|
||||
int get canvasKitMaximumSurfaces =>
|
||||
_configuration?.canvasKitMaximumSurfaces?.toInt() ?? _defaultCanvasKitMaximumSurfaces;
|
||||
static const int _defaultCanvasKitMaximumSurfaces = int.fromEnvironment(
|
||||
|
||||
@ -198,6 +198,28 @@ external DomIntl get domIntl;
|
||||
@JS('Symbol')
|
||||
external DomSymbol get domSymbol;
|
||||
|
||||
@JS('createImageBitmap')
|
||||
external JSPromise _createImageBitmap1(
|
||||
JSAny source,
|
||||
);
|
||||
@JS('createImageBitmap')
|
||||
external JSPromise _createImageBitmap2(
|
||||
JSAny source,
|
||||
JSNumber x,
|
||||
JSNumber y,
|
||||
JSNumber width,
|
||||
JSNumber height,
|
||||
);
|
||||
JSPromise createImageBitmap(JSAny source,
|
||||
[({int x, int y, int width, int height})? bounds]) {
|
||||
if (bounds != null) {
|
||||
return _createImageBitmap2(source, bounds.x.toJS, bounds.y.toJS,
|
||||
bounds.width.toJS, bounds.height.toJS);
|
||||
} else {
|
||||
return _createImageBitmap1(source);
|
||||
}
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomNavigator {}
|
||||
@ -1407,7 +1429,7 @@ extension DomCanvasRenderingContextWebGlExtension
|
||||
class DomCanvasRenderingContextBitmapRenderer {}
|
||||
|
||||
extension DomCanvasRenderingContextBitmapRendererExtension
|
||||
on DomCanvasRenderingContextBitmapRenderer {
|
||||
on DomCanvasRenderingContextBitmapRenderer {
|
||||
external void transferFromImageBitmap(DomImageBitmap bitmap);
|
||||
}
|
||||
|
||||
@ -1415,10 +1437,13 @@ extension DomCanvasRenderingContextBitmapRendererExtension
|
||||
@staticInterop
|
||||
class DomImageData {
|
||||
external factory DomImageData._(JSAny? data, JSNumber sw, JSNumber sh);
|
||||
external factory DomImageData._empty(JSNumber sw, JSNumber sh);
|
||||
}
|
||||
|
||||
DomImageData createDomImageData(Object? data, int sw, int sh) =>
|
||||
DomImageData._(data?.toJSAnyShallow, sw.toJS, sh.toJS);
|
||||
DomImageData createDomImageData(Object data, int sw, int sh) =>
|
||||
DomImageData._(data.toJSAnyShallow, sw.toJS, sh.toJS);
|
||||
DomImageData createBlankDomImageData(int sw, int sh) =>
|
||||
DomImageData._empty(sw.toJS, sh.toJS);
|
||||
|
||||
extension DomImageDataExtension on DomImageData {
|
||||
@JS('data')
|
||||
@ -1436,33 +1461,6 @@ extension DomImageBitmapExtension on DomImageBitmap {
|
||||
external void close();
|
||||
}
|
||||
|
||||
|
||||
@JS('createImageBitmap')
|
||||
external JSPromise _createImageBitmap1(
|
||||
JSAny source,
|
||||
);
|
||||
@JS('createImageBitmap')
|
||||
external JSPromise _createImageBitmap2(
|
||||
JSAny source,
|
||||
JSNumber x,
|
||||
JSNumber y,
|
||||
JSNumber width,
|
||||
JSNumber height,
|
||||
);
|
||||
JSPromise createImageBitmap(JSAny source, [({int x, int y, int width, int height})? bounds]) {
|
||||
if (bounds != null) {
|
||||
return _createImageBitmap2(
|
||||
source,
|
||||
bounds.x.toJS,
|
||||
bounds.y.toJS,
|
||||
bounds.width.toJS,
|
||||
bounds.height.toJS
|
||||
);
|
||||
} else {
|
||||
return _createImageBitmap1(source);
|
||||
}
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomCanvasPattern {}
|
||||
@ -1505,7 +1503,8 @@ MockHttpFetchResponseFactory? mockHttpFetchResponseFactory;
|
||||
/// [httpFetchText] instead.
|
||||
Future<HttpFetchResponse> httpFetch(String url) async {
|
||||
if (mockHttpFetchResponseFactory != null) {
|
||||
final MockHttpFetchResponse? response = await mockHttpFetchResponseFactory!(url);
|
||||
final MockHttpFetchResponse? response =
|
||||
await mockHttpFetchResponseFactory!(url);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
@ -1762,8 +1761,7 @@ class MockHttpFetchPayload implements HttpFetchPayload {
|
||||
while (currentIndex < totalLength) {
|
||||
final int chunkSize = math.min(_chunkSize, totalLength - currentIndex);
|
||||
final Uint8List chunk = Uint8List.sublistView(
|
||||
_byteBuffer.asByteData(), currentIndex, currentIndex + chunkSize
|
||||
);
|
||||
_byteBuffer.asByteData(), currentIndex, currentIndex + chunkSize);
|
||||
callback(chunk.toJS as T);
|
||||
currentIndex += chunkSize;
|
||||
}
|
||||
@ -1773,10 +1771,12 @@ class MockHttpFetchPayload implements HttpFetchPayload {
|
||||
Future<ByteBuffer> asByteBuffer() async => _byteBuffer;
|
||||
|
||||
@override
|
||||
Future<dynamic> json() async => throw AssertionError('json not supported by mock');
|
||||
Future<dynamic> json() async =>
|
||||
throw AssertionError('json not supported by mock');
|
||||
|
||||
@override
|
||||
Future<String> text() async => throw AssertionError('text not supported by mock');
|
||||
Future<String> text() async =>
|
||||
throw AssertionError('text not supported by mock');
|
||||
}
|
||||
|
||||
/// Indicates a missing HTTP payload when one was expected, such as when
|
||||
@ -2311,9 +2311,7 @@ DomBlob createDomBlob(List<Object?> parts, [Map<String, dynamic>? options]) {
|
||||
return DomBlob(parts.toJSAnyShallow as JSArray);
|
||||
} else {
|
||||
return DomBlob.withOptions(
|
||||
parts.toJSAnyShallow as JSArray,
|
||||
options.toJSAnyDeep
|
||||
);
|
||||
parts.toJSAnyShallow as JSArray, options.toJSAnyDeep);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2845,6 +2843,13 @@ extension DomOffscreenCanvasExtension on DomOffscreenCanvas {
|
||||
}
|
||||
}
|
||||
|
||||
WebGLContext getGlContext(int majorVersion) {
|
||||
if (majorVersion == 1) {
|
||||
return getContext('webgl')! as WebGLContext;
|
||||
}
|
||||
return getContext('webgl2')! as WebGLContext;
|
||||
}
|
||||
|
||||
@JS('convertToBlob')
|
||||
external JSPromise _convertToBlob1();
|
||||
@JS('convertToBlob')
|
||||
@ -2858,6 +2863,11 @@ extension DomOffscreenCanvasExtension on DomOffscreenCanvas {
|
||||
}
|
||||
return js_util.promiseToFuture(blob);
|
||||
}
|
||||
|
||||
@JS('transferToImageBitmap')
|
||||
external JSAny? _transferToImageBitmap();
|
||||
DomImageBitmap transferToImageBitmap() =>
|
||||
_transferToImageBitmap()! as DomImageBitmap;
|
||||
}
|
||||
|
||||
DomOffscreenCanvas createDomOffscreenCanvas(int width, int height) =>
|
||||
@ -3451,8 +3461,8 @@ class DomSegments {}
|
||||
|
||||
extension DomSegmentsExtension on DomSegments {
|
||||
DomIteratorWrapper<DomSegment> iterator() {
|
||||
final DomIterator segmentIterator =
|
||||
js_util.callMethod(this, domSymbol.iterator, const <Object?>[]) as DomIterator;
|
||||
final DomIterator segmentIterator = js_util
|
||||
.callMethod(this, domSymbol.iterator, const <Object?>[]) as DomIterator;
|
||||
return DomIteratorWrapper<DomSegment>(segmentIterator);
|
||||
}
|
||||
}
|
||||
@ -3589,10 +3599,8 @@ external JSAny? get _finalizationRegistryConstructor;
|
||||
// dart2js that causes a crash in the Google3 build if we do use a factory
|
||||
// constructor. See b/284478971
|
||||
DomFinalizationRegistry createDomFinalizationRegistry(JSFunction cleanup) =>
|
||||
js_util.callConstructor(
|
||||
_finalizationRegistryConstructor!.toObjectShallow,
|
||||
<Object>[cleanup]
|
||||
);
|
||||
js_util.callConstructor(
|
||||
_finalizationRegistryConstructor!.toObjectShallow, <Object>[cleanup]);
|
||||
|
||||
extension DomFinalizationRegistryExtension on DomFinalizationRegistry {
|
||||
@JS('register')
|
||||
@ -3601,11 +3609,12 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry {
|
||||
@JS('register')
|
||||
external JSVoid _register2(JSAny target, JSAny value, JSAny token);
|
||||
void register(Object target, Object value, [Object? token]) {
|
||||
if (token != null) {
|
||||
_register2(target.toJSAnyShallow, value.toJSAnyShallow, token.toJSAnyShallow);
|
||||
} else {
|
||||
_register1(target.toJSAnyShallow, value.toJSAnyShallow);
|
||||
}
|
||||
if (token != null) {
|
||||
_register2(
|
||||
target.toJSAnyShallow, value.toJSAnyShallow, token.toJSAnyShallow);
|
||||
} else {
|
||||
_register1(target.toJSAnyShallow, value.toJSAnyShallow);
|
||||
}
|
||||
}
|
||||
|
||||
@JS('unregister')
|
||||
@ -3617,6 +3626,11 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry {
|
||||
bool browserSupportsFinalizationRegistry =
|
||||
_finalizationRegistryConstructor != null;
|
||||
|
||||
@JS('window.OffscreenCanvas')
|
||||
external JSAny? get _offscreenCanvasConstructor;
|
||||
|
||||
bool browserSupportsOffscreenCanvas = _offscreenCanvasConstructor != null;
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
extension JSArrayExtension on JSArray {
|
||||
|
||||
@ -163,7 +163,7 @@ void testMain() {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/121758
|
||||
test('resources used in temporary surfaces for Image.toByteData can cross to rendering overlays', () async {
|
||||
final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer;
|
||||
SurfaceFactory.instance.debugClear();
|
||||
RenderCanvasFactory.instance.debugClear();
|
||||
|
||||
ui_web.platformViewRegistry.registerViewFactory(
|
||||
'test-platform-view',
|
||||
|
||||
@ -24,7 +24,7 @@ void setUpCanvasKitTest() {
|
||||
|
||||
tearDown(() {
|
||||
HtmlViewEmbedder.instance.debugClear();
|
||||
SurfaceFactory.instance.debugClear();
|
||||
RenderCanvasFactory.instance.debugClear();
|
||||
});
|
||||
|
||||
setUp(() =>
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
@ -42,10 +41,10 @@ void testMain() {
|
||||
// The platform view is now split in two parts. The contents live
|
||||
// as a child of the glassPane, and the slot lives in the glassPane
|
||||
// shadow root. The slot is the one that has pointer events auto.
|
||||
final DomElement contents = flutterViewEmbedder.glassPaneElement
|
||||
.querySelector('#view-0')!;
|
||||
final DomElement slot = flutterViewEmbedder.sceneElement!
|
||||
.querySelector('slot')!;
|
||||
final DomElement contents =
|
||||
flutterViewEmbedder.glassPaneElement.querySelector('#view-0')!;
|
||||
final DomElement slot =
|
||||
flutterViewEmbedder.sceneElement!.querySelector('slot')!;
|
||||
final DomElement contentsHost = contents.parent!;
|
||||
final DomElement slotHost = slot.parent!;
|
||||
|
||||
@ -292,8 +291,7 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('renders overlays on top of platform views', () async {
|
||||
expect(SurfaceFactory.instance.debugCacheSize, 0);
|
||||
expect(configuration.canvasKitMaximumSurfaces, 8);
|
||||
expect(RenderCanvasFactory.instance.debugCacheSize, 0);
|
||||
final CkPicture testPicture =
|
||||
paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) {
|
||||
canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint());
|
||||
@ -339,8 +337,8 @@ void testMain() {
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_overlay,
|
||||
]);
|
||||
|
||||
// Frame 2:
|
||||
@ -372,7 +370,7 @@ void testMain() {
|
||||
]);
|
||||
|
||||
// Frame 4:
|
||||
// Render: more platform views than max cache size.
|
||||
// Render: more platform views than max overlay count.
|
||||
// Expect: main canvas, backup overlay, maximum overlays.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
renderTestScene(viewCount: 16);
|
||||
@ -391,16 +389,16 @@ void testMain() {
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
// Frame 5:
|
||||
@ -477,7 +475,6 @@ void testMain() {
|
||||
// Render: Views 1-10
|
||||
// Expect: main canvas plus platform view overlays; empty cache.
|
||||
renderTestScene(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
expect(SurfaceFactory.instance.numAvailableOverlays, 0);
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
@ -493,10 +490,10 @@ void testMain() {
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
// Frame 2:
|
||||
@ -504,7 +501,6 @@ void testMain() {
|
||||
// Expect: main canvas plus platform view overlays; empty cache.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
renderTestScene(<int>[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
||||
expect(SurfaceFactory.instance.numAvailableOverlays, 0);
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
@ -520,10 +516,10 @@ void testMain() {
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
// Frame 3:
|
||||
@ -546,10 +542,10 @@ void testMain() {
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
// Frame 4:
|
||||
@ -572,10 +568,10 @@ void testMain() {
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
// TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867
|
||||
@ -599,8 +595,7 @@ void testMain() {
|
||||
]);
|
||||
|
||||
expect(
|
||||
flutterViewEmbedder.glassPaneElement
|
||||
.querySelector('flt-platform-view'),
|
||||
flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'),
|
||||
isNotNull,
|
||||
);
|
||||
|
||||
@ -615,13 +610,14 @@ void testMain() {
|
||||
]);
|
||||
|
||||
expect(
|
||||
flutterViewEmbedder.glassPaneElement
|
||||
.querySelector('flt-platform-view'),
|
||||
flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not crash when resizing the window after textures have been registered', () async {
|
||||
test(
|
||||
'does not crash when resizing the window after textures have been registered',
|
||||
() async {
|
||||
ui_web.platformViewRegistry.registerViewFactory(
|
||||
'test-platform-view',
|
||||
(int viewId) => createDomHTMLDivElement()..id = 'view-0',
|
||||
@ -664,7 +660,7 @@ void testMain() {
|
||||
|
||||
window.debugPhysicalSizeOverride = null;
|
||||
window.debugForceResize();
|
||||
// ImageDecoder is not supported in Safari or Firefox.
|
||||
// ImageDecoder is not supported in Safari or Firefox.
|
||||
}, skip: isSafari || isFirefox);
|
||||
|
||||
test('removed the DOM node of an unrendered platform view', () async {
|
||||
@ -686,8 +682,7 @@ void testMain() {
|
||||
]);
|
||||
|
||||
expect(
|
||||
flutterViewEmbedder.glassPaneElement
|
||||
.querySelector('flt-platform-view'),
|
||||
flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'),
|
||||
isNotNull,
|
||||
);
|
||||
|
||||
@ -744,8 +739,8 @@ void testMain() {
|
||||
rasterizer.draw(sb.build().layerTree);
|
||||
}
|
||||
|
||||
final DomNode skPathDefs = flutterViewEmbedder.sceneElement!
|
||||
.querySelector('#sk_path_defs')!;
|
||||
final DomNode skPathDefs =
|
||||
flutterViewEmbedder.sceneElement!.querySelector('#sk_path_defs')!;
|
||||
|
||||
expect(skPathDefs.childNodes, hasLength(0));
|
||||
|
||||
@ -782,121 +777,6 @@ void testMain() {
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not crash when overlays are disabled', () async {
|
||||
final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer;
|
||||
HtmlViewEmbedder.debugDisableOverlays = true;
|
||||
ui_web.platformViewRegistry.registerViewFactory(
|
||||
'test-platform-view',
|
||||
(int viewId) => createDomHTMLDivElement()..id = 'view-0',
|
||||
);
|
||||
await createPlatformView(0, 'test-platform-view');
|
||||
|
||||
final LayerSceneBuilder sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
sb.addPlatformView(0, width: 10, height: 10);
|
||||
sb.pop();
|
||||
// The below line should not throw an error.
|
||||
rasterizer.draw(sb.build().layerTree);
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
]);
|
||||
HtmlViewEmbedder.debugDisableOverlays = false;
|
||||
});
|
||||
|
||||
test('works correctly with max overlays == 2', () async {
|
||||
final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer;
|
||||
debugOverrideJsConfiguration(
|
||||
<String, Object?>{
|
||||
'canvasKitMaximumSurfaces': 2,
|
||||
}.jsify() as JsFlutterConfiguration?
|
||||
);
|
||||
expect(configuration.canvasKitMaximumSurfaces, 2);
|
||||
expect(configuration.canvasKitVariant, isNot(CanvasKitVariant.auto));
|
||||
|
||||
SurfaceFactory.instance.debugClear();
|
||||
|
||||
expect(SurfaceFactory.instance.maximumSurfaces, 2);
|
||||
expect(SurfaceFactory.instance.maximumOverlays, 1);
|
||||
|
||||
ui_web.platformViewRegistry.registerViewFactory(
|
||||
'test-platform-view',
|
||||
(int viewId) => createDomHTMLDivElement()..id = 'view-0',
|
||||
);
|
||||
await createPlatformView(0, 'test-platform-view');
|
||||
await createPlatformView(1, 'test-platform-view');
|
||||
|
||||
LayerSceneBuilder sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
sb.addPlatformView(0, width: 10, height: 10);
|
||||
sb.pop();
|
||||
// The below line should not throw an error.
|
||||
rasterizer.draw(sb.build().layerTree);
|
||||
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
_overlay,
|
||||
]);
|
||||
|
||||
sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
sb.addPlatformView(1, width: 10, height: 10);
|
||||
sb.addPlatformView(0, width: 10, height: 10);
|
||||
sb.pop();
|
||||
// The below line should not throw an error.
|
||||
rasterizer.draw(sb.build().layerTree);
|
||||
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
_overlay,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
// Reset configuration
|
||||
debugOverrideJsConfiguration(null);
|
||||
});
|
||||
|
||||
test(
|
||||
'correctly renders when overlays are disabled and a subset '
|
||||
'of views is used', () async {
|
||||
final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer;
|
||||
HtmlViewEmbedder.debugDisableOverlays = true;
|
||||
ui_web.platformViewRegistry.registerViewFactory(
|
||||
'test-platform-view',
|
||||
(int viewId) => createDomHTMLDivElement()..id = 'view-0',
|
||||
);
|
||||
await createPlatformView(0, 'test-platform-view');
|
||||
await createPlatformView(1, 'test-platform-view');
|
||||
|
||||
LayerSceneBuilder sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
sb.addPlatformView(0, width: 10, height: 10);
|
||||
sb.addPlatformView(1, width: 10, height: 10);
|
||||
sb.pop();
|
||||
// The below line should not throw an error.
|
||||
rasterizer.draw(sb.build().layerTree);
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
sb.addPlatformView(1, width: 10, height: 10);
|
||||
sb.pop();
|
||||
// The below line should not throw an error.
|
||||
rasterizer.draw(sb.build().layerTree);
|
||||
_expectSceneMatches(<_EmbeddedViewMarker>[
|
||||
_overlay,
|
||||
_platformView,
|
||||
]);
|
||||
|
||||
HtmlViewEmbedder.debugDisableOverlays = false;
|
||||
});
|
||||
|
||||
test('does not create overlays for invisible platform views', () async {
|
||||
final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer;
|
||||
ui_web.platformViewRegistry.registerViewFactory(
|
||||
@ -957,7 +837,9 @@ void testMain() {
|
||||
_overlay,
|
||||
_platformView,
|
||||
_overlay,
|
||||
], reason: 'Overlays created after each group containing a visible view.');
|
||||
],
|
||||
reason:
|
||||
'Overlays created after each group containing a visible view.');
|
||||
|
||||
sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
@ -1059,7 +941,9 @@ void testMain() {
|
||||
_platformView,
|
||||
_platformView,
|
||||
_platformView,
|
||||
], reason: 'Many invisible views can be rendered on top of the base overlay.');
|
||||
],
|
||||
reason:
|
||||
'Many invisible views can be rendered on top of the base overlay.');
|
||||
|
||||
sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
@ -1108,19 +992,22 @@ enum _EmbeddedViewMarker {
|
||||
_EmbeddedViewMarker get _overlay => _EmbeddedViewMarker.overlay;
|
||||
_EmbeddedViewMarker get _platformView => _EmbeddedViewMarker.platformView;
|
||||
|
||||
const Map<String, _EmbeddedViewMarker> _tagToViewMarker = <String, _EmbeddedViewMarker>{
|
||||
const Map<String, _EmbeddedViewMarker> _tagToViewMarker =
|
||||
<String, _EmbeddedViewMarker>{
|
||||
'flt-canvas-container': _EmbeddedViewMarker.overlay,
|
||||
'flt-platform-view-slot': _EmbeddedViewMarker.platformView,
|
||||
};
|
||||
|
||||
void _expectSceneMatches(List<_EmbeddedViewMarker> expectedMarkers, {
|
||||
void _expectSceneMatches(
|
||||
List<_EmbeddedViewMarker> expectedMarkers, {
|
||||
String? reason,
|
||||
}) {
|
||||
// Convert the scene elements to its corresponding array of _EmbeddedViewMarker
|
||||
final List<_EmbeddedViewMarker> sceneElements = flutterViewEmbedder
|
||||
.sceneElement!.children
|
||||
.where((DomElement element) => element.tagName != 'svg')
|
||||
.map((DomElement element) => _tagToViewMarker[element.tagName.toLowerCase()]!)
|
||||
.map((DomElement element) =>
|
||||
_tagToViewMarker[element.tagName.toLowerCase()]!)
|
||||
.toList();
|
||||
|
||||
expect(sceneElements, expectedMarkers, reason: reason);
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
// 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 'common.dart';
|
||||
|
||||
const MethodCodec codec = StandardMethodCodec();
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('$RenderCanvasFactory', () {
|
||||
setUpCanvasKitTest();
|
||||
|
||||
test('getCanvas', () {
|
||||
final RenderCanvasFactory factory = RenderCanvasFactory();
|
||||
expect(factory.baseCanvas, isNotNull);
|
||||
|
||||
expect(factory.debugSurfaceCount, equals(1));
|
||||
|
||||
// Get a canvas from the factory, it should be unique.
|
||||
final RenderCanvas newCanvas = factory.getCanvas();
|
||||
expect(newCanvas, isNot(equals(factory.baseCanvas)));
|
||||
|
||||
expect(factory.debugSurfaceCount, equals(2));
|
||||
|
||||
// Get another canvas from the factory. Now we are at maximum capacity.
|
||||
final RenderCanvas anotherCanvas = factory.getCanvas();
|
||||
expect(anotherCanvas, isNot(equals(factory.baseCanvas)));
|
||||
|
||||
expect(factory.debugSurfaceCount, equals(3));
|
||||
});
|
||||
|
||||
test('releaseCanvas', () {
|
||||
final RenderCanvasFactory factory = RenderCanvasFactory();
|
||||
|
||||
// Create a new canvas and immediately release it.
|
||||
final RenderCanvas canvas = factory.getCanvas();
|
||||
factory.releaseCanvas(canvas);
|
||||
|
||||
// If we create a new canvas, it should be the same as the one we
|
||||
// just created.
|
||||
final RenderCanvas newCanvas = factory.getCanvas();
|
||||
expect(newCanvas, equals(canvas));
|
||||
});
|
||||
|
||||
test('isLive', () {
|
||||
final RenderCanvasFactory factory = RenderCanvasFactory();
|
||||
|
||||
expect(factory.isLive(factory.baseCanvas), isTrue);
|
||||
|
||||
final RenderCanvas canvas = factory.getCanvas();
|
||||
expect(factory.isLive(canvas), isTrue);
|
||||
|
||||
factory.releaseCanvas(canvas);
|
||||
expect(factory.isLive(canvas), isFalse);
|
||||
});
|
||||
|
||||
test('hot restart', () {
|
||||
void expectDisposed(RenderCanvas canvas) {
|
||||
expect(canvas.canvasElement.isConnected, isFalse);
|
||||
}
|
||||
|
||||
final RenderCanvasFactory originalFactory = RenderCanvasFactory.instance;
|
||||
expect(RenderCanvasFactory.debugUninitializedInstance, isNotNull);
|
||||
|
||||
// Cause the surface and its canvas to be attached to the page
|
||||
CanvasKitRenderer.instance.sceneHost!
|
||||
.prepend(originalFactory.baseCanvas.htmlElement);
|
||||
expect(originalFactory.baseCanvas.canvasElement.isConnected, isTrue);
|
||||
|
||||
// Create a few overlay canvases
|
||||
final List<RenderCanvas> overlays = <RenderCanvas>[];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final RenderCanvas canvas = originalFactory.getCanvas();
|
||||
CanvasKitRenderer.instance.sceneHost!.prepend(canvas.htmlElement);
|
||||
overlays.add(canvas);
|
||||
}
|
||||
expect(originalFactory.debugSurfaceCount, 4);
|
||||
|
||||
// Trigger hot restart clean-up logic and check that we indeed clean up.
|
||||
debugEmulateHotRestart();
|
||||
expect(RenderCanvasFactory.debugUninitializedInstance, isNull);
|
||||
expectDisposed(originalFactory.baseCanvas);
|
||||
overlays.forEach(expectDisposed);
|
||||
expect(originalFactory.debugSurfaceCount, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
// 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:js_interop';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('CanvasKit', () {
|
||||
setUpCanvasKitTest();
|
||||
setUp(() async {
|
||||
window.debugOverrideDevicePixelRatio(1.0);
|
||||
});
|
||||
|
||||
Future<DomImageBitmap> newBitmap(int width, int height) async {
|
||||
return (await createImageBitmap(
|
||||
createBlankDomImageData(width, height) as JSAny, (
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
)).toDart)! as DomImageBitmap;
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/75286
|
||||
test('updates canvas logical size when device-pixel ratio changes',
|
||||
() async {
|
||||
final RenderCanvas canvas = RenderCanvas();
|
||||
canvas.render(await newBitmap(10, 16));
|
||||
|
||||
expect(canvas.canvasElement.width, 10);
|
||||
expect(canvas.canvasElement.height, 16);
|
||||
expect(canvas.canvasElement.style.width, '10px');
|
||||
expect(canvas.canvasElement.style.height, '16px');
|
||||
|
||||
// Increase device-pixel ratio: this makes CSS pixels bigger, so we need
|
||||
// fewer of them to cover the browser window.
|
||||
window.debugOverrideDevicePixelRatio(2.0);
|
||||
canvas.render(await newBitmap(10, 16));
|
||||
expect(canvas.canvasElement.width, 10);
|
||||
expect(canvas.canvasElement.height, 16);
|
||||
expect(canvas.canvasElement.style.width, '5px');
|
||||
expect(canvas.canvasElement.style.height, '8px');
|
||||
|
||||
// Decrease device-pixel ratio: this makes CSS pixels smaller, so we need
|
||||
// more of them to cover the browser window.
|
||||
window.debugOverrideDevicePixelRatio(0.5);
|
||||
canvas.render(await newBitmap(10, 16));
|
||||
expect(canvas.canvasElement.width, 10);
|
||||
expect(canvas.canvasElement.height, 16);
|
||||
expect(canvas.canvasElement.style.width, '20px');
|
||||
expect(canvas.canvasElement.style.height, '32px');
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,103 +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 'common.dart';
|
||||
|
||||
const MethodCodec codec = StandardMethodCodec();
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('$SurfaceFactory', () {
|
||||
setUpCanvasKitTest();
|
||||
|
||||
test('cannot be created with size less than 1', () {
|
||||
expect(SurfaceFactory(-1).maximumSurfaces, 1);
|
||||
expect(SurfaceFactory(0).maximumSurfaces, 1);
|
||||
expect(SurfaceFactory(1).maximumSurfaces, 1);
|
||||
expect(SurfaceFactory(2).maximumSurfaces, 2);
|
||||
});
|
||||
|
||||
test('getSurface', () {
|
||||
final SurfaceFactory factory = SurfaceFactory(3);
|
||||
expect(factory.baseSurface, isNotNull);
|
||||
|
||||
expect(factory.debugSurfaceCount, equals(1));
|
||||
|
||||
// Get a surface from the factory, it should be unique.
|
||||
final Surface? newSurface = factory.getSurface();
|
||||
expect(newSurface, isNot(equals(factory.baseSurface)));
|
||||
|
||||
expect(factory.debugSurfaceCount, equals(2));
|
||||
|
||||
// Get another surface from the factory. Now we are at maximum capacity.
|
||||
final Surface? anotherSurface = factory.getSurface();
|
||||
expect(anotherSurface, isNot(equals(factory.baseSurface)));
|
||||
|
||||
expect(factory.debugSurfaceCount, equals(3));
|
||||
});
|
||||
|
||||
test('releaseSurface', () {
|
||||
final SurfaceFactory factory = SurfaceFactory(3);
|
||||
|
||||
// Create a new surface and immediately release it.
|
||||
final Surface? surface = factory.getSurface();
|
||||
factory.releaseSurface(surface!);
|
||||
|
||||
// If we create a new surface, it should be the same as the one we
|
||||
// just created.
|
||||
final Surface? newSurface = factory.getSurface();
|
||||
expect(newSurface, equals(surface));
|
||||
});
|
||||
|
||||
test('isLive', () {
|
||||
final SurfaceFactory factory = SurfaceFactory(3);
|
||||
|
||||
expect(factory.isLive(factory.baseSurface), isTrue);
|
||||
|
||||
final Surface? surface = factory.getSurface();
|
||||
expect(factory.isLive(surface!), isTrue);
|
||||
|
||||
factory.releaseSurface(surface);
|
||||
expect(factory.isLive(surface), isFalse);
|
||||
});
|
||||
|
||||
test('hot restart', () {
|
||||
void expectDisposed(Surface surface) {
|
||||
expect(surface.htmlCanvas!.isConnected, isFalse);
|
||||
}
|
||||
|
||||
final SurfaceFactory originalFactory = SurfaceFactory.instance;
|
||||
expect(SurfaceFactory.debugUninitializedInstance, isNotNull);
|
||||
|
||||
// Cause the surface and its canvas to be attached to the page
|
||||
originalFactory.baseSurface.acquireFrame(const ui.Size(10, 10));
|
||||
originalFactory.baseSurface.addToScene();
|
||||
expect(originalFactory.baseSurface.htmlCanvas!.isConnected, isTrue);
|
||||
|
||||
// Create a few overlay surfaces
|
||||
final List<Surface> overlays = <Surface>[];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
overlays.add(originalFactory.getSurface()!
|
||||
..acquireFrame(const ui.Size(10, 10))
|
||||
..addToScene());
|
||||
}
|
||||
expect(originalFactory.debugSurfaceCount, 4);
|
||||
|
||||
// Trigger hot restart clean-up logic and check that we indeed clean up.
|
||||
debugEmulateHotRestart();
|
||||
expect(SurfaceFactory.debugUninitializedInstance, isNull);
|
||||
expectDisposed(originalFactory.baseSurface);
|
||||
overlays.forEach(expectDisposed);
|
||||
expect(originalFactory.debugSurfaceCount, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -23,17 +23,14 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('Surface allocates canvases efficiently', () {
|
||||
final Surface? surface = SurfaceFactory.instance.getSurface();
|
||||
final Surface surface = Surface();
|
||||
final CkSurface originalSurface =
|
||||
surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface;
|
||||
final DomCanvasElement original = surface.htmlCanvas!;
|
||||
surface.acquireFrame(const ui.Size(9, 19)).skiaSurface;
|
||||
final DomOffscreenCanvas original = surface.debugOffscreenCanvas!;
|
||||
|
||||
// Expect exact requested dimensions.
|
||||
expect(original.width, 9);
|
||||
expect(original.height, 19);
|
||||
expect(original.style.width, '9px');
|
||||
expect(original.style.height, '19px');
|
||||
expect(original.style.transform, _isTranslate('0', '0'));
|
||||
expect(originalSurface.width(), 9);
|
||||
expect(originalSurface.height(), 19);
|
||||
|
||||
@ -41,11 +38,8 @@ void testMain() {
|
||||
// Skia renders into the visible area.
|
||||
final CkSurface shrunkSurface =
|
||||
surface.acquireFrame(const ui.Size(5, 15)).skiaSurface;
|
||||
final DomCanvasElement shrunk = surface.htmlCanvas!;
|
||||
final DomOffscreenCanvas shrunk = surface.debugOffscreenCanvas!;
|
||||
expect(shrunk, same(original));
|
||||
expect(shrunk.style.width, '9px');
|
||||
expect(shrunk.style.height, '19px');
|
||||
expect(shrunk.style.transform, _isTranslate('0', '-4'));
|
||||
expect(shrunkSurface, isNot(same(originalSurface)));
|
||||
expect(shrunkSurface.width(), 5);
|
||||
expect(shrunkSurface.height(), 15);
|
||||
@ -54,52 +48,42 @@ void testMain() {
|
||||
// by 40% to accommodate future increases.
|
||||
final CkSurface firstIncreaseSurface =
|
||||
surface.acquireFrame(const ui.Size(10, 20)).skiaSurface;
|
||||
final DomCanvasElement firstIncrease = surface.htmlCanvas!;
|
||||
final DomOffscreenCanvas firstIncrease = surface.debugOffscreenCanvas!;
|
||||
expect(firstIncrease, same(original));
|
||||
expect(firstIncreaseSurface, isNot(same(shrunkSurface)));
|
||||
|
||||
// Expect overallocated dimensions
|
||||
expect(firstIncrease.width, 14);
|
||||
expect(firstIncrease.height, 28);
|
||||
expect(firstIncrease.style.width, '14px');
|
||||
expect(firstIncrease.style.height, '28px');
|
||||
expect(firstIncrease.style.transform, _isTranslate('0', '-8'));
|
||||
expect(firstIncreaseSurface.width(), 10);
|
||||
expect(firstIncreaseSurface.height(), 20);
|
||||
|
||||
// Subsequent increases within 40% reuse the old canvas.
|
||||
final CkSurface secondIncreaseSurface =
|
||||
surface.acquireFrame(const ui.Size(11, 22)).skiaSurface;
|
||||
final DomCanvasElement secondIncrease = surface.htmlCanvas!;
|
||||
final DomOffscreenCanvas secondIncrease = surface.debugOffscreenCanvas!;
|
||||
expect(secondIncrease, same(firstIncrease));
|
||||
expect(secondIncrease.style.transform, _isTranslate('0', '-6'));
|
||||
expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface)));
|
||||
expect(secondIncreaseSurface.width(), 11);
|
||||
expect(secondIncreaseSurface.height(), 22);
|
||||
|
||||
// Increases beyond the 40% limit will cause a new allocation.
|
||||
final CkSurface hugeSurface = surface.acquireFrame(const ui.Size(20, 40)).skiaSurface;
|
||||
final DomCanvasElement huge = surface.htmlCanvas!;
|
||||
final DomOffscreenCanvas huge = surface.debugOffscreenCanvas!;
|
||||
expect(huge, same(secondIncrease));
|
||||
expect(hugeSurface, isNot(same(secondIncreaseSurface)));
|
||||
|
||||
// Also over-allocated
|
||||
expect(huge.width, 28);
|
||||
expect(huge.height, 56);
|
||||
expect(huge.style.width, '28px');
|
||||
expect(huge.style.height, '56px');
|
||||
expect(huge.style.transform, _isTranslate('0', '-16'));
|
||||
expect(hugeSurface.width(), 20);
|
||||
expect(hugeSurface.height(), 40);
|
||||
|
||||
// Shrink again. Reuse the last allocated surface.
|
||||
final CkSurface shrunkSurface2 =
|
||||
surface.acquireFrame(const ui.Size(5, 15)).skiaSurface;
|
||||
final DomCanvasElement shrunk2 = surface.htmlCanvas!;
|
||||
final DomOffscreenCanvas shrunk2 = surface.debugOffscreenCanvas!;
|
||||
expect(shrunk2, same(huge));
|
||||
expect(shrunk2.style.width, '28px');
|
||||
expect(shrunk2.style.height, '56px');
|
||||
expect(shrunk2.style.transform, _isTranslate('0', '-41'));
|
||||
expect(shrunkSurface2, isNot(same(hugeSurface)));
|
||||
expect(shrunkSurface2.width(), 5);
|
||||
expect(shrunkSurface2.height(), 15);
|
||||
@ -109,11 +93,8 @@ void testMain() {
|
||||
window.debugOverrideDevicePixelRatio(2.0);
|
||||
final CkSurface dpr2Surface2 =
|
||||
surface.acquireFrame(const ui.Size(5, 15)).skiaSurface;
|
||||
final DomCanvasElement dpr2Canvas = surface.htmlCanvas!;
|
||||
final DomOffscreenCanvas dpr2Canvas = surface.debugOffscreenCanvas!;
|
||||
expect(dpr2Canvas, same(huge));
|
||||
expect(dpr2Canvas.style.width, '14px');
|
||||
expect(dpr2Canvas.style.height, '28px');
|
||||
expect(dpr2Canvas.style.transform, _isTranslate('0', '-20.5'));
|
||||
expect(dpr2Surface2, isNot(same(hugeSurface)));
|
||||
expect(dpr2Surface2.width(), 5);
|
||||
expect(dpr2Surface2.height(), 15);
|
||||
@ -123,13 +104,13 @@ void testMain() {
|
||||
// which cannot be a different size from the canvas.
|
||||
// TODO(hterkelsen): See if we can give a custom size for software
|
||||
// surfaces.
|
||||
}, skip: isFirefox);
|
||||
}, skip: isFirefox || !Surface.offscreenCanvasSupported);
|
||||
|
||||
test(
|
||||
'Surface creates new context when WebGL context is restored',
|
||||
() async {
|
||||
final Surface? surface = SurfaceFactory.instance.getSurface();
|
||||
expect(surface!.debugForceNewContext, isTrue);
|
||||
final Surface surface = Surface();
|
||||
expect(surface.debugForceNewContext, isTrue);
|
||||
final CkSurface before =
|
||||
surface.acquireFrame(const ui.Size(9, 19)).skiaSurface;
|
||||
expect(surface.debugForceNewContext, isFalse);
|
||||
@ -142,8 +123,7 @@ void testMain() {
|
||||
expect(afterAcquireFrame, same(before));
|
||||
|
||||
// Emulate WebGL context loss.
|
||||
final DomCanvasElement canvas =
|
||||
surface.htmlElement.children.single as DomCanvasElement;
|
||||
final DomOffscreenCanvas canvas = surface.debugOffscreenCanvas!;
|
||||
final Object ctx = canvas.getContext('webgl2')!;
|
||||
final Object loseContextExtension = js_util.callMethod(
|
||||
ctx,
|
||||
@ -172,7 +152,7 @@ void testMain() {
|
||||
expect(afterContextLost, isNot(same(before)));
|
||||
},
|
||||
// Firefox can't create a WebGL2 context in headless mode.
|
||||
skip: isFirefox,
|
||||
skip: isFirefox || !Surface.offscreenCanvasSupported,
|
||||
);
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/75286
|
||||
@ -183,9 +163,8 @@ void testMain() {
|
||||
|
||||
expect(original.width(), 10);
|
||||
expect(original.height(), 16);
|
||||
expect(surface.htmlCanvas!.style.width, '10px');
|
||||
expect(surface.htmlCanvas!.style.height, '16px');
|
||||
expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0'));
|
||||
expect(surface.debugOffscreenCanvas!.width, 10);
|
||||
expect(surface.debugOffscreenCanvas!.height, 16);
|
||||
|
||||
// Increase device-pixel ratio: this makes CSS pixels bigger, so we need
|
||||
// fewer of them to cover the browser window.
|
||||
@ -194,9 +173,8 @@ void testMain() {
|
||||
surface.acquireFrame(const ui.Size(10, 16)).skiaSurface;
|
||||
expect(highDpr.width(), 10);
|
||||
expect(highDpr.height(), 16);
|
||||
expect(surface.htmlCanvas!.style.width, '5px');
|
||||
expect(surface.htmlCanvas!.style.height, '8px');
|
||||
expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0'));
|
||||
expect(surface.debugOffscreenCanvas!.width, 10);
|
||||
expect(surface.debugOffscreenCanvas!.height, 16);
|
||||
|
||||
// Decrease device-pixel ratio: this makes CSS pixels smaller, so we need
|
||||
// more of them to cover the browser window.
|
||||
@ -205,9 +183,8 @@ void testMain() {
|
||||
surface.acquireFrame(const ui.Size(10, 16)).skiaSurface;
|
||||
expect(lowDpr.width(), 10);
|
||||
expect(lowDpr.height(), 16);
|
||||
expect(surface.htmlCanvas!.style.width, '20px');
|
||||
expect(surface.htmlCanvas!.style.height, '32px');
|
||||
expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0'));
|
||||
expect(surface.debugOffscreenCanvas!.width, 10);
|
||||
expect(surface.debugOffscreenCanvas!.height, 16);
|
||||
|
||||
// See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172
|
||||
window.debugOverrideDevicePixelRatio(2.0);
|
||||
@ -215,28 +192,10 @@ void testMain() {
|
||||
surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface;
|
||||
expect(changeRatioAndSize.width(), 10);
|
||||
expect(changeRatioAndSize.height(), 16);
|
||||
expect(surface.htmlCanvas!.style.width, '5px');
|
||||
expect(surface.htmlCanvas!.style.height, '8px');
|
||||
expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0'));
|
||||
});
|
||||
expect(surface.debugOffscreenCanvas!.width, 10);
|
||||
expect(surface.debugOffscreenCanvas!.height, 16);
|
||||
},
|
||||
skip: !Surface.offscreenCanvasSupported,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Checks that the CSS 'transform' property is a translation in a cross-browser way.
|
||||
///
|
||||
/// Takes strings directly to avoid issues with floating point or differences
|
||||
/// in stringification of numeric values across JS and Wasm targets.
|
||||
Matcher _isTranslate(String x, String y) {
|
||||
// When the y coordinate is zero, Firefox omits it, e.g.:
|
||||
// Chrome/Safari/Edge: translate(0px, 0px)
|
||||
// Firefox: translate(0px)
|
||||
final String fullFormat = 'translate(${x}px, ${y}px)';
|
||||
if (y != '0') {
|
||||
return equals(fullFormat);
|
||||
} else {
|
||||
return anyOf(
|
||||
fullFormat, // Non-Firefox browsers use this format.
|
||||
'translate(${x}px)', // Firefox omits y when it's zero.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,9 +19,8 @@ void main() {
|
||||
}
|
||||
|
||||
class StubPictureRenderer implements PictureRenderer {
|
||||
final DomCanvasElement scratchCanvasElement = createDomCanvasElement(
|
||||
width: 500, height: 500
|
||||
);
|
||||
final DomCanvasElement scratchCanvasElement =
|
||||
createDomCanvasElement(width: 500, height: 500);
|
||||
|
||||
@override
|
||||
Future<DomImageBitmap> renderPicture(ScenePicture picture) async {
|
||||
@ -63,9 +62,11 @@ void testMain() {
|
||||
final List<DomElement> children = sceneElement.children.toList();
|
||||
expect(children.length, 1);
|
||||
final DomElement containerElement = children.first;
|
||||
expect(containerElement.tagName, equalsIgnoringCase('flt-canvas-container'));
|
||||
expect(
|
||||
containerElement.tagName, equalsIgnoringCase('flt-canvas-container'));
|
||||
|
||||
final List<DomElement> containerChildren = containerElement.children.toList();
|
||||
final List<DomElement> containerChildren =
|
||||
containerElement.children.toList();
|
||||
expect(containerChildren.length, 1);
|
||||
final DomElement canvasElement = containerChildren.first;
|
||||
final DomCSSStyleDeclaration style = canvasElement.style;
|
||||
@ -81,12 +82,11 @@ void testMain() {
|
||||
debugOverrideDevicePixelRatio(2.0);
|
||||
|
||||
final PlatformView platformView = PlatformView(
|
||||
1,
|
||||
const ui.Size(100, 120),
|
||||
const PlatformViewStyling(
|
||||
position: PlatformViewPosition.offset(ui.Offset(50, 80)),
|
||||
)
|
||||
);
|
||||
1,
|
||||
const ui.Size(100, 120),
|
||||
const PlatformViewStyling(
|
||||
position: PlatformViewPosition.offset(ui.Offset(50, 80)),
|
||||
));
|
||||
final EngineRootLayer rootLayer = EngineRootLayer();
|
||||
rootLayer.slices.add(PlatformViewSlice(<PlatformView>[platformView], null));
|
||||
final EngineScene scene = EngineScene(rootLayer);
|
||||
@ -96,7 +96,8 @@ void testMain() {
|
||||
final List<DomElement> children = sceneElement.children.toList();
|
||||
expect(children.length, 1);
|
||||
final DomElement containerElement = children.first;
|
||||
expect(containerElement.tagName, equalsIgnoringCase('flt-platform-view-slot'));
|
||||
expect(
|
||||
containerElement.tagName, equalsIgnoringCase('flt-platform-view-slot'));
|
||||
|
||||
final DomCSSStyleDeclaration style = containerElement.style;
|
||||
expect(style.left, '25px');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user