Unverified Commit 55fe41be authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

[web] New HtmlElementView.fromTagName constructor (#130513)

This wraps up the platform view improvements discussed in https://github.com/flutter/flutter/issues/127030.

- Splits `HtmlElementView` into 2 files that are conditionally imported.
- The non-web version can be instantiated but it throws if it ends up being built in a widget tree.
- Out-of-the-box view factories that create visible & invisible DOM elements given a `tagName` parameter.
- New `HtmlElementView.fromTagName()` constructor that uses the default factories to create DOM elements.
- Tests covering the new API.

Depends on https://github.com/flutter/engine/pull/43828

Fixes https://github.com/flutter/flutter/issues/127030
parent f054f5aa
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: prefer_const_constructors_in_immutables
// ignore_for_file: avoid_unused_constructor_parameters
import 'framework.dart';
import 'platform_view.dart';
/// The platform-specific implementation of [HtmlElementView].
extension HtmlElementViewImpl on HtmlElementView {
/// Creates an [HtmlElementView] that renders a DOM element with the given
/// [tagName].
static HtmlElementView createFromTagName({
Key? key,
required String tagName,
bool isVisible = true,
ElementCreatedCallback? onElementCreated,
}) {
throw UnimplementedError('HtmlElementView is only available on Flutter Web');
}
/// Called from [HtmlElementView.build] to build the widget tree.
///
/// This is not expected to be invoked in non-web environments. It throws if
/// that happens.
///
/// The implementation on Flutter Web builds a platform view and handles its
/// lifecycle.
Widget buildImpl(BuildContext context) {
throw UnimplementedError('HtmlElementView is only available on Flutter Web');
}
}
// Copyright 2014 The Flutter 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:ui_web' as ui_web;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'framework.dart';
import 'platform_view.dart';
/// The platform-specific implementation of [HtmlElementView].
extension HtmlElementViewImpl on HtmlElementView {
/// Creates an [HtmlElementView] that renders a DOM element with the given
/// [tagName].
static HtmlElementView createFromTagName({
Key? key,
required String tagName,
bool isVisible = true,
ElementCreatedCallback? onElementCreated,
}) {
return HtmlElementView(
key: key,
viewType: isVisible ? ui_web.PlatformViewRegistry.defaultVisibleViewType : ui_web.PlatformViewRegistry.defaultInvisibleViewType,
onPlatformViewCreated: _createPlatformViewCallbackForElementCallback(onElementCreated),
creationParams: <dynamic, dynamic>{'tagName': tagName},
);
}
/// The implementation of [HtmlElementView.build].
///
/// This is not expected to be invoked in non-web environments. It throws if
/// that happens.
///
/// The implementation on Flutter Web builds an HTML platform view and handles
/// its lifecycle.
Widget buildImpl(BuildContext context) {
return PlatformViewLink(
viewType: viewType,
onCreatePlatformView: _createController,
surfaceFactory: (BuildContext context, PlatformViewController controller) {
return PlatformViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);
}
/// Creates the controller and kicks off its initialization.
_HtmlElementViewController _createController(
PlatformViewCreationParams params,
) {
final _HtmlElementViewController controller = _HtmlElementViewController(
params.id,
viewType,
creationParams,
);
controller._initialize().then((_) {
params.onPlatformViewCreated(params.id);
onPlatformViewCreated?.call(params.id);
});
return controller;
}
}
PlatformViewCreatedCallback? _createPlatformViewCallbackForElementCallback(
ElementCreatedCallback? onElementCreated,
) {
if (onElementCreated == null) {
return null;
}
return (int id) {
onElementCreated(_platformViewsRegistry.getViewById(id));
};
}
class _HtmlElementViewController extends PlatformViewController {
_HtmlElementViewController(
this.viewId,
this.viewType,
this.creationParams,
);
@override
final int viewId;
/// The unique identifier for the HTML view type to be embedded by this widget.
///
/// A PlatformViewFactory for this type must have been registered.
final String viewType;
final dynamic creationParams;
bool _initialized = false;
Future<void> _initialize() async {
final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': viewType,
'params': creationParams,
};
await SystemChannels.platform_views.invokeMethod<void>('create', args);
_initialized = true;
}
@override
Future<void> clearFocus() async {
// Currently this does nothing on Flutter Web.
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
}
@override
Future<void> dispatchPointerEvent(PointerEvent event) async {
// We do not dispatch pointer events to HTML views because they may contain
// cross-origin iframes, which only accept user-generated events.
}
@override
Future<void> dispose() async {
if (_initialized) {
await SystemChannels.platform_views.invokeMethod<void>('dispose', viewId);
}
}
}
/// Overrides the [ui_web.PlatformViewRegistry] used by [HtmlElementView].
///
/// This is used for testing view factory registration.
@visibleForTesting
ui_web.PlatformViewRegistry? debugOverridePlatformViewRegistry;
ui_web.PlatformViewRegistry get _platformViewsRegistry => debugOverridePlatformViewRegistry ?? ui_web.platformViewRegistry;
......@@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import '_html_element_view_io.dart' if (dart.library.js_util) '_html_element_view_web.dart';
import 'basic.dart';
import 'debug.dart';
import 'focus_manager.dart';
......@@ -324,6 +325,14 @@ class UiKitView extends _DarwinView {
State<UiKitView> createState() => _UiKitViewState();
}
/// Callback signature for when the platform view's DOM element was created.
///
/// [element] is the DOM element that was created.
///
/// Also see [HtmlElementView.fromTagName] that uses this callback
/// signature.
typedef ElementCreatedCallback = void Function(Object element);
/// Embeds an HTML element in the Widget hierarchy in Flutter Web.
///
/// *NOTE*: This only works in Flutter Web. To embed web content on other
......@@ -368,6 +377,52 @@ class HtmlElementView extends StatelessWidget {
this.creationParams,
});
/// Creates a platform view that creates a DOM element specified by [tagName].
///
/// [isVisible] indicates whether the view is visible to the user or not.
/// Setting this to false allows the rendering pipeline to perform extra
/// optimizations knowing that the view will not result in any pixels painted
/// on the screen.
///
/// [onElementCreated] is called when the DOM element is created. It can be
/// used by the app to customize the element by adding attributes and styles.
///
/// ```dart
/// import 'package:flutter/widgets.dart';
/// import 'package:web/web.dart' as web;
///
/// // ...
///
/// class MyWidget extends StatelessWidget {
/// const MyWidget({super.key});
///
/// @override
/// Widget build(BuildContext context) {
/// return HtmlElementView.fromTagName(
/// tagName: 'div',
/// onElementCreated: (Object element) {
/// element as web.HTMLElement;
/// element.style
/// ..backgroundColor = 'blue'
/// ..border = '1px solid red';
/// },
/// );
/// }
/// }
/// ```
factory HtmlElementView.fromTagName({
Key? key,
required String tagName,
bool isVisible = true,
ElementCreatedCallback? onElementCreated,
}) =>
HtmlElementViewImpl.createFromTagName(
key: key,
tagName: tagName,
isVisible: isVisible,
onElementCreated: onElementCreated,
);
/// The unique identifier for the HTML view type to be embedded by this widget.
///
/// A PlatformViewFactory for this type must have been registered.
......@@ -382,83 +437,7 @@ class HtmlElementView extends StatelessWidget {
final Object? creationParams;
@override
Widget build(BuildContext context) {
assert(kIsWeb, 'HtmlElementView is only available on Flutter Web.');
return PlatformViewLink(
viewType: viewType,
onCreatePlatformView: _createHtmlElementView,
surfaceFactory: (BuildContext context, PlatformViewController controller) {
return PlatformViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);
}
/// Creates the controller and kicks off its initialization.
_HtmlElementViewController _createHtmlElementView(PlatformViewCreationParams params) {
final _HtmlElementViewController controller = _HtmlElementViewController(
params.id,
viewType,
creationParams,
);
controller._initialize().then((_) {
params.onPlatformViewCreated(params.id);
onPlatformViewCreated?.call(params.id);
});
return controller;
}
}
class _HtmlElementViewController extends PlatformViewController {
_HtmlElementViewController(
this.viewId,
this.viewType,
this.creationParams,
);
@override
final int viewId;
/// The unique identifier for the HTML view type to be embedded by this widget.
///
/// A PlatformViewFactory for this type must have been registered.
final String viewType;
final dynamic creationParams;
bool _initialized = false;
Future<void> _initialize() async {
final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': viewType,
'params': creationParams,
};
await SystemChannels.platform_views.invokeMethod<void>('create', args);
_initialized = true;
}
@override
Future<void> clearFocus() async {
// Currently this does nothing on Flutter Web.
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
}
@override
Future<void> dispatchPointerEvent(PointerEvent event) async {
// We do not dispatch pointer events to HTML views because they may contain
// cross-origin iframes, which only accept user-generated events.
}
@override
Future<void> dispose() async {
if (_initialized) {
await SystemChannels.platform_views.invokeMethod<void>('dispose', viewId);
}
}
Widget build(BuildContext context) => buildImpl(context);
}
class _AndroidViewState extends State<AndroidView> {
......
......@@ -471,77 +471,6 @@ class FakeIosPlatformViewsController {
}
}
class FakeHtmlPlatformViewsController {
FakeHtmlPlatformViewsController() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall);
}
Iterable<FakeHtmlPlatformView> get views => _views.values;
final Map<int, FakeHtmlPlatformView> _views = <int, FakeHtmlPlatformView>{};
final Set<String> _registeredViewTypes = <String>{};
late Completer<void> resizeCompleter;
Completer<void>? createCompleter;
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
Future<dynamic> _onMethodCall(MethodCall call) {
switch (call.method) {
case 'create':
return _create(call);
case 'dispose':
return _dispose(call);
}
return Future<dynamic>.sync(() => null);
}
Future<dynamic> _create(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
final int id = args['id'] as int;
final String viewType = args['viewType'] as String;
final Object? params = args['params'];
if (_views.containsKey(id)) {
throw PlatformException(
code: 'error',
message: 'Trying to create an already created platform view, view id: $id',
);
}
if (!_registeredViewTypes.contains(viewType)) {
throw PlatformException(
code: 'error',
message: 'Trying to create a platform view of unregistered type: $viewType',
);
}
if (createCompleter != null) {
await createCompleter!.future;
}
_views[id] = FakeHtmlPlatformView(id, viewType, params);
return Future<int?>.sync(() => null);
}
Future<dynamic> _dispose(MethodCall call) {
final int id = call.arguments as int;
if (!_views.containsKey(id)) {
throw PlatformException(
code: 'error',
message: 'Trying to dispose a platform view with unknown id: $id',
);
}
_views.remove(id);
return Future<dynamic>.sync(() => null);
}
}
@immutable
class FakeAndroidPlatformView {
const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection,
......@@ -656,31 +585,3 @@ class FakeUiKitView {
return 'FakeUiKitView(id: $id, type: $type, creationParams: $creationParams)';
}
}
@immutable
class FakeHtmlPlatformView {
const FakeHtmlPlatformView(this.id, this.type, [this.creationParams]);
final int id;
final String type;
final Object? creationParams;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FakeHtmlPlatformView
&& other.id == id
&& other.type == type
&& other.creationParams == creationParams;
}
@override
int get hashCode => Object.hash(id, type, creationParams);
@override
String toString() {
return 'FakeHtmlPlatformView(id: $id, type: $type, params: $creationParams)';
}
}
......@@ -3189,7 +3189,7 @@ void main() {
// This file runs on non-web platforms, so we expect `HtmlElementView` to
// fail.
final dynamic exception = tester.takeException();
expect(exception, isAssertionError);
expect(exception, isUnimplementedError);
expect(exception.toString(), contains('HtmlElementView is only available on Flutter Web'));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment