blob: 894acec1ec98dfbb125812285668619878df6f28 [file] [log] [blame]
// Copyright 2018 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';
import 'package:fidl_fuchsia_math/fidl_async.dart';
import 'package:fidl_fuchsia_ui_gfx/fidl_async.dart' show ImportToken;
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewHolderToken;
import 'package:fidl_fuchsia_ui_viewsv1/fidl_async.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:zircon/zircon.dart';
import 'internal/_child_scene_layer.dart';
import 'view_container.dart' as shared;
import 'view_container_listener_impl.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 {
// TODO consider providing this API after MS-2293 is fixed
// 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();
// }
// }
// TODO consider providing this API after MS-2293 is fixed
// factory ChildViewConnection.connect(Services services,
// {InterfaceRequest<ServiceProvider> childServices,
// ChildViewConnectionCallback onAvailable,
// ChildViewConnectionCallback onUnavailable}) {
// final app.ViewProviderProxy viewProvider = new app.ViewProviderProxy();
// services.connectToService(viewProvider.ctrl);
// try {
// EventPairPair viewTokens = new EventPairPair();
// assert(viewTokens.status == ZX.OK);
// viewProvider.createView(viewTokens.second, childServices, null);
// return new ChildViewConnection.fromViewHolderToken(viewTokens.first,
// onAvailable: onAvailable, onUnavailable: onUnavailable);
// } finally {
// viewProvider.ctrl.close();
// }
// }
final ChildViewConnectionCallback _onAvailableCallback;
final ChildViewConnectionCallback _onUnavailableCallback;
ViewHolderToken _viewHolderToken;
int _viewKey;
ViewProperties _currentViewProperties;
bool _available = false;
VoidCallback _onViewInfoAvailable;
SceneHost _sceneHost;
int _attachments = 0;
/// Constructs |ChildViewConnection| from a token.
ChildViewConnection(ViewHolderToken viewHolderToken,
{ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable})
: _onAvailableCallback = onAvailable ?? _emptyConnectionCallback,
_onUnavailableCallback = onUnavailable ?? _emptyConnectionCallback,
_viewHolderToken = viewHolderToken {
assert(_viewHolderToken?.value != null);
}
/// Deprecated.
ChildViewConnection.fromViewHolderToken(EventPair viewHolderToken,
{ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable})
: this(ViewHolderToken(value: viewHolderToken),
onAvailable: onAvailable, onUnavailable: onUnavailable);
/// Deprecated.
ChildViewConnection.fromImportToken(ImportToken viewHolderToken,
{ChildViewConnectionCallback onAvailable,
ChildViewConnectionCallback onUnavailable})
: this(ViewHolderToken(value: viewHolderToken.value),
onAvailable: onAvailable, onUnavailable: onUnavailable);
bool get _attached => _attachments > 0;
/// TODO add documnetation
@override
void onAvailable() {
_available = true;
if (_onViewInfoAvailable != null) {
_onViewInfoAvailable();
}
_onAvailableCallback(this);
}
/// TODO add documentation
@override
void onUnavailable() {
_available = false;
_onUnavailableCallback(this);
}
/// 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 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.
/// TODO add documentation
void sendSizeChangeHintHack(
double widthChangeFactor, double heightChangeFactor) {
assert(_attached);
assert(_attachments == 1);
if (_viewKey == null) {
return;
}
if (shared.globalViewContainer == null) {
return;
}
shared.globalViewContainer.sendSizeChangeHintHack(
_viewKey, widthChangeFactor, heightChangeFactor);
}
void _addChildToViewHost() {
if (shared.globalViewContainer == null) {
return;
}
assert(_viewHolderToken.value.isValid);
assert(_attached);
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 = SceneHost(sceneTokens.first.passHandle());
_viewKey = shared.nextGlobalViewKey();
shared.globalViewContainer
.addChild2(_viewKey, _viewHolderToken.value, sceneTokens.second);
_viewHolderToken = ViewHolderToken(value: EventPair(null));
assert(
!ViewContainerListenerImpl.instance.containsConnectionForKey(_viewKey));
ViewContainerListenerImpl.instance.addConnectionForKey(_viewKey, this);
}
void _attach() {
assert(_attachments >= 0);
++_attachments;
if (_viewKey == null) {
_addChildToViewHost();
}
}
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 _detach() {
assert(_attached);
--_attachments;
scheduleMicrotask(_removeChildFromViewHostIfNeeded);
}
void _removeChildFromViewHost() {
if (shared.globalViewContainer == null) {
return;
}
assert(!_viewHolderToken.value.isValid);
assert(!_attached);
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 = ViewHolderToken(value: viewTokens.first);
shared.globalViewContainer.removeChild2(_viewKey, viewTokens.second);
_viewKey = null;
_available = false;
_currentViewProperties = null;
_sceneHost.dispose();
_sceneHost = null;
}
void _removeChildFromViewHostIfNeeded() {
assert(_attachments >= 0);
if (_attachments == 0 && _viewKey != null) {
_removeChildFromViewHost();
}
}
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 (shared.globalViewContainer == null) {
return;
}
ViewProperties viewProperties = _createViewProperties(
width, height, insetTop, insetRight, insetBottom, insetLeft, focusable);
if (viewProperties == null) {
return;
}
shared.globalViewContainer.setChildProperties(_viewKey, viewProperties);
}
}
/// TODO add documentation
class RenderChildView extends RenderBox {
ChildViewConnection _connection;
bool _hitTestable;
bool _focusable;
TextPainter _debugErrorMessage;
double _width;
double _height;
/// Creates a child view render object.
RenderChildView({
ChildViewConnection connection,
bool hitTestable = true,
bool focusable = true,
}) : _connection = connection,
_hitTestable = hitTestable,
_focusable = focusable,
assert(hitTestable != null);
@override
bool get alwaysNeedsCompositing => true;
/// The child to display.
ChildViewConnection get connection => _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 able to recieve focus events
bool get focusable => _focusable;
set focusable(bool value) {
assert(value != null);
if (value == _focusable) {
return;
}
_focusable = value;
if (_connection != null) {
markNeedsPaint();
}
}
/// Whether this child should be included during hit testing.
bool get hitTestable => _hitTestable;
set hitTestable(bool value) {
assert(value != null);
if (value == _hitTestable) {
return;
}
_hitTestable = 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 debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(
DiagnosticsProperty<ChildViewConnection>(
'connection',
connection,
),
);
}
@override
void detach() {
if (_connection != null) {
_connection._detach();
assert(_connection._onViewInfoAvailable != null);
_connection._onViewInfoAvailable = null;
}
super.detach();
}
@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 (shared.globalViewContainer == null) {
context.canvas
.drawRect(offset & size, Paint()..color = Color(0xFF0000FF));
_debugErrorMessage.paint(context.canvas, offset);
}
return true;
}());
}
@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 (shared.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;
}());
}
}