blob: 7b5fe9bafa35e31c3a70fa28ad3d0b61975508e0 [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui;
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_sys/fidl.dart';
import 'package:fidl_fuchsia_math/fidl_async.dart';
import 'package:fidl_fuchsia_ui_app/fidl.dart' as app;
import 'package:fidl_fuchsia_ui_viewsv1/fidl_async.dart';
import 'package:fidl_fuchsia_ui_viewsv1token/fidl.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:fuchsia_scenic_flutter/view_container.dart'
show
nextGlobalViewKey,
globalViewContainer,
ViewContainerListenerDelegate,
ViewContainerListenerImpl;
import 'package:lib.app.dart/app.dart';
import 'package:meta/meta.dart';
import 'package:zircon/zircon.dart';
import 'mozart.dart';
typedef ChildViewConnectionCallback = void Function(
ChildViewConnection connection);
void _emptyConnectionCallback(ChildViewConnection c) {}
/// A connection with a child view.
///
/// Used with the [ChildView] widget to display a child view.
class ChildViewConnection implements ViewContainerListenerDelegate {
ChildViewConnection(InterfaceHandle<ViewOwner> viewOwner,
{ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable})
: this.fromViewHolderToken(
EventPair(viewOwner?.passChannel()?.passHandle()),
onAvailable: onAvailable,
onUnavailable: onUnavailable);
ChildViewConnection.fromViewHolderToken(EventPair viewHolderToken,
{ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable})
: _onAvailableCallback = onAvailable ?? _emptyConnectionCallback,
_onUnavailableCallback = onUnavailable ?? _emptyConnectionCallback,
_viewHolderToken = viewHolderToken {
assert(_viewHolderToken != null);
}
factory ChildViewConnection.launch(String url, Launcher launcher,
{InterfaceRequest<ComponentController> controller,
InterfaceRequest<ServiceProvider> childServices,
ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable}) {
final Services services = Services();
final LaunchInfo launchInfo =
LaunchInfo(url: url, directoryRequest: services.request());
try {
launcher.createComponent(launchInfo, controller);
return ChildViewConnection.connect(services,
childServices: childServices,
onAvailable: onAvailable,
onUnavailable: onUnavailable);
} finally {
services.close();
}
}
factory ChildViewConnection.connect(Services services,
{InterfaceRequest<ServiceProvider> childServices,
ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable}) {
final app.ViewProviderProxy viewProvider = app.ViewProviderProxy();
services.connectToService(viewProvider.ctrl);
try {
EventPairPair viewTokens = EventPairPair();
assert(viewTokens.status == ZX.OK);
viewProvider.createView(viewTokens.second, childServices, null);
return ChildViewConnection.fromViewHolderToken(viewTokens.first,
onAvailable: onAvailable, onUnavailable: onUnavailable);
} finally {
viewProvider.ctrl.close();
}
}
final ChildViewConnectionCallback _onAvailableCallback;
final ChildViewConnectionCallback _onUnavailableCallback;
EventPair _viewHolderToken;
int _viewKey;
ViewProperties _currentViewProperties;
bool _available = false;
VoidCallback _onViewInfoAvailable;
ui.SceneHost _sceneHost;
@override
void onAvailable() {
_available = true;
if (_onViewInfoAvailable != null) {
_onViewInfoAvailable();
}
_onAvailableCallback(this);
}
@override
void onUnavailable() {
_available = false;
_onUnavailableCallback(this);
}
void _addChildToViewHost() {
if (globalViewContainer == null) {
return;
}
assert(_attached);
assert(_viewHolderToken != null);
assert(_viewKey == null);
assert(!_available);
assert(_sceneHost == null);
final EventPairPair sceneTokens = EventPairPair();
assert(sceneTokens.status == ZX.OK);
// Analyzer doesn't know Handle must be dart:zircon's Handle
_sceneHost = ui.SceneHost(sceneTokens.first.passHandle());
_viewKey = nextGlobalViewKey();
globalViewContainer.addChild2(
_viewKey, _viewHolderToken, sceneTokens.second);
_viewHolderToken = null;
assert(
!ViewContainerListenerImpl.instance.containsConnectionForKey(_viewKey));
ViewContainerListenerImpl.instance.addConnectionForKey(_viewKey, this);
}
void _removeChildFromViewHost() {
if (globalViewContainer == null) {
return;
}
assert(!_attached);
assert(_viewHolderToken == null);
assert(_viewKey != null);
assert(_sceneHost != null);
assert(ViewContainerListenerImpl.instance.getConnectionForKey(_viewKey) ==
this);
final EventPairPair viewTokens = EventPairPair();
assert(viewTokens.status == ZX.OK);
ViewContainerListenerImpl.instance.removeConnectionForKey(_viewKey);
_viewHolderToken = viewTokens.first;
globalViewContainer.removeChild2(_viewKey, viewTokens.second);
_viewKey = null;
_available = false;
_currentViewProperties = null;
_sceneHost.dispose();
_sceneHost = null;
}
/// Only call when the connection is available.
void requestFocus() {
if (_viewKey != null) {
// TODO(SCN-1186): Use new mechanism to implement RequestFocus.
}
}
// The number of render objects attached to this view. In between frames, we
// might have more than one connected if we get added to a new render object
// before we get removed from the old render object. By the time we get around
// to computing our layout, we must be back to just having one render object.
int _attachments = 0;
bool get _attached => _attachments > 0;
void _attach() {
assert(_attachments >= 0);
++_attachments;
if (_viewKey == null) {
_addChildToViewHost();
}
}
void _detach() {
assert(_attached);
--_attachments;
scheduleMicrotask(_removeChildFromViewHostIfNeeded);
}
void _removeChildFromViewHostIfNeeded() {
assert(_attachments >= 0);
if (_attachments == 0 && _viewKey != null) {
_removeChildFromViewHost();
}
}
void sendSizeChangeHintHack(
double widthChangeFactor, double heightChangeFactor) {
assert(_attached);
assert(_attachments == 1);
if (_viewKey == null) {
return;
}
if (globalViewContainer == null) {
return;
}
globalViewContainer.sendSizeChangeHintHack(
_viewKey, widthChangeFactor, heightChangeFactor);
}
ViewProperties _createViewProperties(
double width,
double height,
double insetTop,
double insetRight,
double insetBottom,
double insetLeft,
bool focusable) {
if (_currentViewProperties != null &&
_currentViewProperties.viewLayout.size.width == width &&
_currentViewProperties.viewLayout.size.height == height &&
_currentViewProperties.viewLayout.inset.top == insetTop &&
_currentViewProperties.viewLayout.inset.right == insetRight &&
_currentViewProperties.viewLayout.inset.bottom == insetBottom &&
_currentViewProperties.viewLayout.inset.left == insetLeft &&
(_currentViewProperties.customFocusBehavior == null ||
_currentViewProperties.customFocusBehavior.allowFocus) ==
focusable) {
return null;
}
SizeF size = SizeF(width: width, height: height);
InsetF inset = InsetF(
top: insetTop, right: insetRight, bottom: insetBottom, left: insetLeft);
ViewLayout viewLayout = ViewLayout(size: size, inset: inset);
final customFocusBehavior = CustomFocusBehavior(allowFocus: focusable);
return _currentViewProperties = ViewProperties(
viewLayout: viewLayout,
customFocusBehavior: customFocusBehavior,
);
}
void _setChildProperties(
double width,
double height,
double insetTop,
double insetRight,
double insetBottom,
double insetLeft,
bool focusable,
) {
assert(_attached);
assert(_attachments == 1);
assert(_viewKey != null);
if (globalViewContainer == null) {
return;
}
ViewProperties viewProperties = _createViewProperties(
width, height, insetTop, insetRight, insetBottom, insetLeft, focusable);
if (viewProperties == null) {
return;
}
globalViewContainer.setChildProperties(_viewKey, viewProperties);
}
}
class _RenderChildView extends RenderBox {
/// Creates a child view render object.
_RenderChildView({
ChildViewConnection connection,
bool hitTestable = true,
bool focusable = true,
}) : _connection = connection,
_hitTestable = hitTestable,
_focusable = focusable,
assert(hitTestable != null);
/// The child to display.
ChildViewConnection get connection => _connection;
ChildViewConnection _connection;
set connection(ChildViewConnection value) {
if (value == _connection) {
return;
}
if (attached && _connection != null) {
_connection._detach();
assert(_connection._onViewInfoAvailable != null);
_connection._onViewInfoAvailable = null;
}
_connection = value;
if (attached && _connection != null) {
_connection._attach();
assert(_connection._onViewInfoAvailable == null);
_connection._onViewInfoAvailable = markNeedsPaint;
}
if (_connection == null) {
markNeedsPaint();
} else {
markNeedsLayout();
}
}
/// Whether this child should be included during hit testing.
bool get hitTestable => _hitTestable;
bool _hitTestable;
set hitTestable(bool value) {
assert(value != null);
if (value == _hitTestable) {
return;
}
_hitTestable = value;
if (_connection != null) {
markNeedsPaint();
}
}
/// Whether this child should be able to recieve focus events
bool get focusable => _focusable;
bool _focusable;
set focusable(bool value) {
assert(value != null);
if (value == _focusable) {
return;
}
_focusable = value;
if (_connection != null) {
markNeedsPaint();
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if (_connection != null) {
_connection._attach();
assert(_connection._onViewInfoAvailable == null);
_connection._onViewInfoAvailable = markNeedsPaint;
}
}
@override
void detach() {
if (_connection != null) {
_connection._detach();
assert(_connection._onViewInfoAvailable != null);
_connection._onViewInfoAvailable = null;
}
super.detach();
}
@override
bool get alwaysNeedsCompositing => true;
TextPainter _debugErrorMessage;
double _width;
double _height;
@override
void performLayout() {
size = constraints.biggest;
// Ignore if we have no child view connection.
if (_connection == null) {
return;
}
if (_width != null && _height != null) {
double deltaWidth = (_width - size.width).abs();
double deltaHeight = (_height - size.height).abs();
// Ignore insignificant changes in size that are likely rounding errors.
if (deltaWidth < 0.0001 && deltaHeight < 0.0001) {
return;
}
}
_width = size.width;
_height = size.height;
_connection._setChildProperties(
_width, _height, 0.0, 0.0, 0.0, 0.0, _focusable);
assert(() {
if (globalViewContainer == null) {
_debugErrorMessage ??= TextPainter(
text: TextSpan(
text:
'Child views are supported only when running in Scenic.'));
_debugErrorMessage.layout(minWidth: size.width, maxWidth: size.width);
}
return true;
}());
}
@override
bool hitTestSelf(Offset position) => true;
@override
void paint(PaintingContext context, Offset offset) {
assert(needsCompositing);
if (_connection?._available == true) {
context.addLayer(ChildSceneLayer(
offset: offset,
width: _width,
height: _height,
sceneHost: _connection._sceneHost,
hitTestable: hitTestable,
));
}
assert(() {
if (globalViewContainer == null) {
context.canvas.drawRect(
offset & size, Paint()..color = Color(0xFF0000FF));
_debugErrorMessage.paint(context.canvas, offset);
}
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(
DiagnosticsProperty<ChildViewConnection>(
'connection',
connection,
),
);
}
}
/// A layer that represents content from another process.
class ChildSceneLayer extends Layer {
/// Creates a layer that displays content rendered by another process.
///
/// All of the arguments must not be null.
ChildSceneLayer({
this.offset = Offset.zero,
this.width = 0.0,
this.height = 0.0,
this.sceneHost,
this.hitTestable = true,
});
/// Offset from parent in the parent's coordinate system.
Offset offset;
/// The horizontal extent of the child, in logical pixels.
double width;
/// The vertical extent of the child, in logical pixels.
double height;
/// The host site for content rendered by the child.
ui.SceneHost sceneHost;
/// Whether this child should be included during hit testing.
///
/// Defaults to true.
bool hitTestable;
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder,
[Offset layerOffset = Offset.zero]) {
builder.addChildScene(
offset: offset + layerOffset,
width: width,
height: height,
sceneHost: sceneHost,
hitTestable: hitTestable,
);
return null;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description
..add(DiagnosticsProperty<Offset>('offset', offset))
..add(DoubleProperty('width', width))
..add(DoubleProperty('height', height))
..add(DiagnosticsProperty<ui.SceneHost>('sceneHost', sceneHost))
..add(DiagnosticsProperty<bool>('hitTestable', hitTestable));
}
@override
S find<S>(Offset regionOffset) => null;
}
/// A widget that is replaced by content from another process.
///
/// Requires a [MediaQuery] ancestor to provide appropriate media information to
/// the child.
@immutable
class ChildView extends LeafRenderObjectWidget {
/// Creates a widget that is replaced by content from another process.
ChildView({this.connection, this.hitTestable = true, this.focusable = true})
: super(key: GlobalObjectKey(connection));
/// A connection to the child whose content will replace this widget.
final ChildViewConnection connection;
/// Whether this child should be included during hit testing.
///
/// Defaults to true.
final bool hitTestable;
/// Whether this child and its children should be allowed to receive focus.
///
/// Defaults to true.
final bool focusable;
@override
_RenderChildView createRenderObject(BuildContext context) {
return _RenderChildView(
connection: connection,
hitTestable: hitTestable,
focusable: focusable,
);
}
@override
void updateRenderObject(BuildContext context, _RenderChildView renderObject) {
renderObject
..connection = connection
..hitTestable = hitTestable
..focusable = focusable;
}
}
class View {
/// Provide services to Scenic throught |provider|.
///
/// |services| should contain the list of service names offered by the
/// |provider|.
static void offerServiceProvider(
InterfaceHandle<ServiceProvider> provider, List<String> services) {
// Analyzer doesn't know Handle must be dart:zircon's Handle
Scenic.offerServiceProvider(provider.passChannel().handle, services);
}
}