| // 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 = new EventPairPair(); |
| assert(sceneTokens.status == ZX.OK); |
| |
| // Analyzer doesn't know Handle must be dart:zircon's Handle |
| _sceneHost = new 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 = new 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 = const 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: const TextSpan( |
| text: |
| 'Child views are supported only when running in Scenic.')); |
| _debugErrorMessage.layout(minWidth: size.width, maxWidth: size.width); |
| } |
| return true; |
| }()); |
| } |
| } |