blob: 23010c6db0bff58c7863e249cabf4468fdd0aaa2 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
const String kCanvasContainerTag = 'flt-canvas-container';
// This is an interface that renders a `ScenePicture` as a `DomImageBitmap`.
// It is optionally asynchronous. It is required for the `EngineSceneView` to
// composite pictures into the canvases in the DOM tree it builds.
abstract class PictureRenderer {
FutureOr<DomImageBitmap> renderPicture(ScenePicture picture);
}
class _SceneRender {
_SceneRender(this.scene, this._completer) {
scene.beginRender();
}
final EngineScene scene;
final Completer<void> _completer;
void done() {
scene.endRender();
_completer.complete();
}
}
// This class builds a DOM tree that composites an `EngineScene`.
class EngineSceneView {
factory EngineSceneView(PictureRenderer pictureRenderer) {
final DomElement sceneElement = createDomElement('flt-scene');
return EngineSceneView._(pictureRenderer, sceneElement);
}
EngineSceneView._(this.pictureRenderer, this.sceneElement);
final PictureRenderer pictureRenderer;
final DomElement sceneElement;
List<SliceContainer> containers = <SliceContainer>[];
_SceneRender? _currentRender;
_SceneRender? _nextRender;
Future<void> renderScene(EngineScene scene) {
if (_currentRender != null) {
// If a scene is already queued up, drop it and queue this one up instead
// so that the scene view always displays the most recently requested scene.
_nextRender?.done();
final Completer<void> completer = Completer<void>();
_nextRender = _SceneRender(scene, completer);
return completer.future;
}
final Completer<void> completer = Completer<void>();
_currentRender = _SceneRender(scene, completer);
_kickRenderLoop();
return completer.future;
}
Future<void> _kickRenderLoop() async {
final _SceneRender current = _currentRender!;
await _renderScene(current.scene);
current.done();
_currentRender = _nextRender;
_nextRender = null;
if (_currentRender == null) {
return;
} else {
return _kickRenderLoop();
}
}
Future<void> _renderScene(EngineScene scene) async {
final List<LayerSlice> slices = scene.rootLayer.slices;
final Iterable<Future<DomImageBitmap?>> renderFutures = slices.map(
(LayerSlice slice) async => switch (slice) {
PlatformViewSlice() => null,
PictureSlice() => pictureRenderer.renderPicture(slice.picture),
}
);
final List<DomImageBitmap?> renderedBitmaps = await Future.wait(renderFutures);
final List<SliceContainer?> reusableContainers = List<SliceContainer?>.from(containers);
final List<SliceContainer> newContainers = <SliceContainer>[];
for (int i = 0; i < slices.length; i++) {
final LayerSlice slice = slices[i];
switch (slice) {
case PictureSlice():
PictureSliceContainer? container;
for (int j = 0; j < reusableContainers.length; j++) {
final SliceContainer? candidate = reusableContainers[j];
if (candidate is PictureSliceContainer) {
container = candidate;
reusableContainers[j] = null;
break;
}
}
if (container != null) {
container.bounds = slice.picture.cullRect;
} else {
container = PictureSliceContainer(slice.picture.cullRect);
}
container.updateContents();
container.renderBitmap(renderedBitmaps[i]!);
newContainers.add(container);
case PlatformViewSlice():
for (final PlatformView view in slice.views) {
// TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`,
// instead of using `EnginePlatformDispatcher...implicitView` directly,
// or make the FlutterView "register" like in canvaskit.
// Ensure the platform view contents are injected in the DOM.
EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId);
// Attempt to reuse a container for the existing view
PlatformViewContainer? container;
for (int j = 0; j < reusableContainers.length; j++) {
final SliceContainer? candidate = reusableContainers[j];
if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) {
container = candidate;
reusableContainers[j] = null;
break;
}
}
container ??= PlatformViewContainer(view.viewId);
container.size = view.size;
container.styling = view.styling;
container.updateContents();
newContainers.add(container);
}
}
}
containers = newContainers;
DomElement? currentElement = sceneElement.firstElementChild;
for (final SliceContainer container in containers) {
if (currentElement == null) {
sceneElement.appendChild(container.container);
} else if (currentElement == container.container) {
currentElement = currentElement.nextElementSibling;
} else {
sceneElement.insertBefore(container.container, currentElement);
}
}
// Remove any other unused containers
while (currentElement != null) {
final DomElement? sibling = currentElement.nextElementSibling;
sceneElement.removeChild(currentElement);
currentElement = sibling;
}
}
}
sealed class SliceContainer {
DomElement get container;
void updateContents();
}
final class PictureSliceContainer extends SliceContainer {
factory PictureSliceContainer(ui.Rect bounds) {
final DomElement container = domDocument.createElement(kCanvasContainerTag);
final DomCanvasElement canvas = createDomCanvasElement(
width: bounds.width.toInt(),
height: bounds.height.toInt()
);
container.appendChild(canvas);
return PictureSliceContainer._(bounds, container, canvas);
}
PictureSliceContainer._(this._bounds, this.container, this.canvas);
ui.Rect _bounds;
bool _dirty = true;
ui.Rect get bounds => _bounds;
set bounds(ui.Rect bounds) {
if (_bounds != bounds) {
_bounds = bounds;
_dirty = true;
}
}
@override
void updateContents() {
if (_dirty) {
_dirty = false;
final ui.Rect roundedOutBounds = ui.Rect.fromLTRB(
bounds.left.floorToDouble(),
bounds.top.floorToDouble(),
bounds.right.ceilToDouble(),
bounds.bottom.ceilToDouble()
);
final DomCSSStyleDeclaration style = canvas.style;
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
final double logicalWidth = roundedOutBounds.width / devicePixelRatio;
final double logicalHeight = roundedOutBounds.height / devicePixelRatio;
final double logicalLeft = roundedOutBounds.left / devicePixelRatio;
final double logicalTop = roundedOutBounds.top / devicePixelRatio;
style.width = '${logicalWidth}px';
style.height = '${logicalHeight}px';
style.position = 'absolute';
style.left = '${logicalLeft}px';
style.top = '${logicalTop}px';
canvas.width = roundedOutBounds.width.ceilToDouble();
canvas.height = roundedOutBounds.height.ceilToDouble();
}
}
void renderBitmap(DomImageBitmap bitmap) {
final DomCanvasRenderingContextBitmapRenderer ctx = canvas.contextBitmapRenderer;
ctx.transferFromImageBitmap(bitmap);
}
@override
final DomElement container;
final DomCanvasElement canvas;
}
final class PlatformViewContainer extends SliceContainer {
PlatformViewContainer(this.viewId) : container = createPlatformViewSlot(viewId);
final int viewId;
PlatformViewStyling? _styling;
ui.Size? _size;
bool _dirty = false;
@override
final DomElement container;
set styling(PlatformViewStyling styling) {
if (_styling != styling) {
_styling = styling;
_dirty = true;
}
}
set size(ui.Size size) {
if (_size != size) {
_size = size;
_dirty = true;
}
}
@override
void updateContents() {
assert(_styling != null);
assert(_size != null);
if (_dirty) {
final DomCSSStyleDeclaration style = container.style;
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
final double logicalWidth = _size!.width / devicePixelRatio;
final double logicalHeight = _size!.height / devicePixelRatio;
style.width = '${logicalWidth}px';
style.height = '${logicalHeight}px';
style.position = 'absolute';
final ui.Offset? offset = _styling!.position.offset;
final double logicalLeft = (offset?.dx ?? 0) / devicePixelRatio;
final double logicalTop = (offset?.dy ?? 0) / devicePixelRatio;
style.left = '${logicalLeft}px';
style.top = '${logicalTop}px';
final Matrix4? transform = _styling!.position.transform;
style.transform = transform != null ? float64ListToCssTransform3d(transform.storage) : '';
style.opacity = _styling!.opacity != 1.0 ? '${_styling!.opacity}' : '';
// TODO(jacksongardner): Implement clip styling for platform views
_dirty = false;
}
}
}