| // 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); |
| } |
| } |