| // 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:collection'; |
| import 'dart:ui' as ui; |
| |
| import 'package:lib.app.dart/app_async.dart'; |
| import 'package:fidl_fuchsia_sys/fidl_async.dart'; |
| import 'package:fidl_fuchsia_math/fidl_async.dart' as fidl; |
| import 'package:fidl_fuchsia_ui_viewsv1/fidl_async.dart'; |
| import 'package:fidl_fuchsia_ui_viewsv1token/fidl_async.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:fidl/fidl.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:zircon/zircon.dart'; |
| |
| import 'mozart.dart'; |
| |
| export 'package:fidl_fuchsia_ui_viewsv1token/fidl_async.dart' show ViewOwner; |
| |
| ViewContainerProxy _initViewContainer() { |
| // Analyzer doesn't know Handle must be dart:zircon's Handle |
| final Handle handle = ScenicStartupInfo.takeViewContainer(); |
| if (handle == null) { |
| return null; |
| } |
| final ViewContainerProxy proxy = new ViewContainerProxy() |
| ..ctrl.bind(new InterfaceHandle<ViewContainer>(new Channel(handle))) |
| ..setListener(_ViewContainerListenerImpl2.instance.createInterfaceHandle()); |
| |
| assert(() { |
| proxy.ctrl.whenClosed.then((_) async { |
| print('ViewContainerProxy: closed'); |
| }); |
| return true; |
| }()); |
| |
| return proxy; |
| } |
| |
| final ViewContainerProxy _viewContainer = _initViewContainer(); |
| |
| class _ViewContainerListenerImpl2 extends ViewContainerListener { |
| final ViewContainerListenerBinding _binding = |
| new ViewContainerListenerBinding(); |
| |
| InterfaceHandle<ViewContainerListener> createInterfaceHandle() { |
| return _binding.wrap(this); |
| } |
| |
| static final _ViewContainerListenerImpl2 instance = |
| new _ViewContainerListenerImpl2(); |
| |
| @override |
| Future<Null> onChildAttached(int childKey, ViewInfo childViewInfo) async { |
| ChildViewConnection2 connection = _connections[childKey]; |
| connection?._onAttachedToContainer(childViewInfo); |
| } |
| |
| @override |
| Future<Null> onChildUnavailable(int childKey) async { |
| ChildViewConnection2 connection = _connections[childKey]; |
| connection?._onUnavailable(); |
| } |
| |
| final Map<int, ChildViewConnection2> _connections = |
| new HashMap<int, ChildViewConnection2>(); |
| } |
| |
| typedef ChildViewConnection2Callback = void Function( |
| ChildViewConnection2 connection); |
| void _emptyConnectionCallback(ChildViewConnection2 c) {} |
| |
| /// A connection with a child view. |
| /// |
| /// Used with the [ChildView2] widget to display a child view. |
| class ChildViewConnection2 { |
| ChildViewConnection2(this._viewOwner, |
| {ChildViewConnection2Callback onAvailable, |
| ChildViewConnection2Callback onUnavailable}) |
| : _onAvailableCallback = onAvailable ?? _emptyConnectionCallback, |
| _onUnavailableCallback = onUnavailable ?? _emptyConnectionCallback, |
| assert(_viewOwner != null); |
| |
| factory ChildViewConnection2.launch(String url, Launcher launcher, |
| {InterfaceRequest<ComponentController> controller, |
| InterfaceRequest<ServiceProvider> childServices, |
| ChildViewConnection2Callback onAvailable, |
| ChildViewConnection2Callback onUnavailable}) { |
| final Services services = new Services(); |
| final LaunchInfo launchInfo = |
| new LaunchInfo(url: url, directoryRequest: services.request()); |
| try { |
| launcher.createComponent(launchInfo, controller); |
| return new ChildViewConnection2.connect(services, |
| childServices: childServices, |
| onAvailable: onAvailable, |
| onUnavailable: onUnavailable); |
| } finally { |
| services.close(); |
| } |
| } |
| |
| factory ChildViewConnection2.connect(Services services, |
| {InterfaceRequest<ServiceProvider> childServices, |
| ChildViewConnection2Callback onAvailable, |
| ChildViewConnection2Callback onUnavailable}) { |
| final ViewProviderProxy viewProvider = new ViewProviderProxy(); |
| services.connectToService(viewProvider.ctrl); |
| try { |
| final InterfacePair<ViewOwner> viewOwner = new InterfacePair<ViewOwner>(); |
| viewProvider.createView(viewOwner.passRequest(), childServices); |
| return new ChildViewConnection2(viewOwner.passHandle(), |
| onAvailable: onAvailable, onUnavailable: onUnavailable); |
| } finally { |
| viewProvider.ctrl.close(); |
| } |
| } |
| |
| final ChildViewConnection2Callback _onAvailableCallback; |
| final ChildViewConnection2Callback _onUnavailableCallback; |
| InterfaceHandle<ViewOwner> _viewOwner; |
| |
| static int _nextViewKey = 1; |
| int _viewKey; |
| |
| ViewProperties _currentViewProperties; |
| |
| VoidCallback _onViewInfoAvailable; |
| ViewInfo _viewInfo; |
| ui.SceneHost _sceneHost; |
| |
| void _onAttachedToContainer(ViewInfo viewInfo) { |
| assert(_viewInfo == null); |
| _viewInfo = viewInfo; |
| if (_onViewInfoAvailable != null) { |
| _onViewInfoAvailable(); |
| } |
| _onAvailableCallback(this); |
| } |
| |
| void _onUnavailable() { |
| _viewInfo = null; |
| _onUnavailableCallback(this); |
| } |
| |
| void _addChildToViewHost() { |
| if (_viewContainer == null) { |
| return; |
| } |
| assert(_attached); |
| assert(_viewOwner != null); |
| assert(_viewKey == null); |
| assert(_viewInfo == null); |
| assert(_sceneHost == null); |
| |
| final EventPairPair pair = new EventPairPair(); |
| assert(pair.status == ZX.OK); |
| |
| // Analyzer doesn't know Handle must be dart:zircon's Handle |
| _sceneHost = new ui.SceneHost(pair.first.passHandle()); |
| _viewKey = _nextViewKey++; |
| _viewContainer.addChild(_viewKey, _viewOwner, pair.second); |
| _viewOwner = null; |
| assert(!_ViewContainerListenerImpl2.instance._connections |
| .containsKey(_viewKey)); |
| _ViewContainerListenerImpl2.instance._connections[_viewKey] = this; |
| } |
| |
| void _removeChildFromViewHost() { |
| if (_viewContainer == null) { |
| return; |
| } |
| assert(!_attached); |
| assert(_viewOwner == null); |
| assert(_viewKey != null); |
| assert(_sceneHost != null); |
| assert(_ViewContainerListenerImpl2.instance._connections[_viewKey] == this); |
| final ChannelPair pair = new ChannelPair(); |
| assert(pair.status == ZX.OK); |
| _ViewContainerListenerImpl2.instance._connections.remove(_viewKey); |
| _viewOwner = new InterfaceHandle<ViewOwner>(pair.first); |
| _viewContainer.removeChild( |
| _viewKey, new InterfaceRequest<ViewOwner>(pair.second)); |
| _viewKey = null; |
| _viewInfo = null; |
| _currentViewProperties = null; |
| _sceneHost.dispose(); |
| _sceneHost = null; |
| } |
| |
| /// Only call when the connection is available. |
| void requestFocus() { |
| if (_viewKey != null) { |
| _viewContainer.requestFocus(_viewKey); |
| } |
| } |
| |
| // 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(); |
| } |
| } |
| |
| 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; |
| } |
| |
| fidl.SizeF size = new fidl.SizeF(width: width, height: height); |
| fidl.InsetF inset = new fidl.InsetF( |
| top: insetTop, right: insetRight, bottom: insetBottom, left: insetLeft); |
| ViewLayout viewLayout = new ViewLayout(size: size, inset: inset); |
| final customFocusBehavior = new CustomFocusBehavior(allowFocus: focusable); |
| return _currentViewProperties = new 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 (_viewContainer == null) { |
| return; |
| } |
| ViewProperties viewProperties = _createViewProperties( |
| width, height, insetTop, insetRight, insetBottom, insetLeft, focusable); |
| if (viewProperties == null) { |
| return; |
| } |
| _viewContainer.setChildProperties(_viewKey, viewProperties); |
| } |
| } |
| |
| class _RenderChildView2 extends RenderBox { |
| /// Creates a child view render object. |
| _RenderChildView2({ |
| ChildViewConnection2 connection, |
| bool hitTestable = true, |
| bool focusable = true, |
| }) : _connection = connection, |
| _hitTestable = hitTestable, |
| _focusable = focusable, |
| assert(hitTestable != null); |
| |
| /// The child to display. |
| ChildViewConnection2 get connection => _connection; |
| ChildViewConnection2 _connection; |
| set connection(ChildViewConnection2 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 (_viewContainer == null) { |
| _debugErrorMessage ??= new TextPainter( |
| text: const 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?._viewInfo != null) { |
| context.addLayer(new ChildSceneLayer( |
| offset: offset, |
| width: _width, |
| height: _height, |
| sceneHost: _connection._sceneHost, |
| hitTestable: hitTestable, |
| )); |
| } |
| assert(() { |
| if (_viewContainer == null) { |
| context.canvas.drawRect( |
| offset & size, new Paint()..color = const Color(0xFF0000FF)); |
| _debugErrorMessage.paint(context.canvas, offset); |
| } |
| return true; |
| }()); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add( |
| new DiagnosticsProperty<ChildViewConnection2>( |
| '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(new DiagnosticsProperty<Offset>('offset', offset)) |
| ..add(new DoubleProperty('width', width)) |
| ..add(new DoubleProperty('height', height)) |
| ..add(new DiagnosticsProperty<ui.SceneHost>('sceneHost', sceneHost)) |
| ..add(new 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 ChildView2 extends LeafRenderObjectWidget { |
| /// Creates a widget that is replaced by content from another process. |
| ChildView2({this.connection, this.hitTestable = true, this.focusable = true}) |
| : super(key: new GlobalObjectKey(connection)); |
| |
| /// A connection to the child whose content will replace this widget. |
| final ChildViewConnection2 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 |
| _RenderChildView2 createRenderObject(BuildContext context) { |
| return new _RenderChildView2( |
| connection: connection, |
| hitTestable: hitTestable, |
| focusable: focusable, |
| ); |
| } |
| |
| @override |
| void updateRenderObject( |
| BuildContext context, _RenderChildView2 renderObject) { |
| renderObject |
| ..connection = connection |
| ..hitTestable = hitTestable |
| ..focusable = focusable; |
| } |
| } |
| |
| class View2 { |
| /// 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); |
| } |
| } |