Unverified Commit e6e44de3 authored by yaakovschectman's avatar yaakovschectman Committed by GitHub

Add MacOS AppKitView class. (#132583)

Add derived classes from the Darwin platform view base classes for
MacOS. Functionality is largely the same as the `UiKitView`, but the two
are decoupled and and can further diverge in the future as needed. Some
unit tests remain skipped for now as the gesture recognizers for MacOS
are not yet implemented.

https://github.com/flutter/flutter/issues/128519

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat

---------
Co-authored-by: 's avatarChris Bracken <chris@bracken.jp>
parent b4753c32
...@@ -279,7 +279,10 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController> ...@@ -279,7 +279,10 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
RenderDarwinPlatformView({ RenderDarwinPlatformView({
required T viewController, required T viewController,
required this.hitTestBehavior, required this.hitTestBehavior,
}) : _viewController = viewController; required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : _viewController = viewController {
updateGestureRecognizers(gestureRecognizers);
}
/// The unique identifier of the platform view controlled by this controller. /// The unique identifier of the platform view controlled by this controller.
...@@ -313,6 +316,8 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController> ...@@ -313,6 +316,8 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
PointerEvent? _lastPointerDownEvent; PointerEvent? _lastPointerDownEvent;
_UiKitViewGestureRecognizer? _gestureRecognizer;
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
return constraints.biggest; return constraints.biggest;
...@@ -374,6 +379,9 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController> ...@@ -374,6 +379,9 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent); GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
super.detach(); super.detach();
} }
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers);
} }
/// A render object for an iOS UIKit UIView. /// A render object for an iOS UIKit UIView.
...@@ -401,14 +409,11 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> { ...@@ -401,14 +409,11 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
RenderUiKitView({ RenderUiKitView({
required super.viewController, required super.viewController,
required super.hitTestBehavior, required super.hitTestBehavior,
required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers required super.gestureRecognizers,
}) { });
updateGestureRecognizers(gestureRecognizers);
}
// TODO(schectman): Add gesture functionality to macOS platform view when implemented.
// https://github.com/flutter/flutter/issues/128519
/// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers} /// {@macro flutter.rendering.PlatformViewRenderBox.updateGestureRecognizers}
@override
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) { void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert( assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
...@@ -431,8 +436,6 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> { ...@@ -431,8 +436,6 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
_lastPointerDownEvent = event.original ?? event; _lastPointerDownEvent = event.original ?? event;
} }
_UiKitViewGestureRecognizer? _gestureRecognizer;
@override @override
void detach() { void detach() {
_gestureRecognizer!.reset(); _gestureRecognizer!.reset();
...@@ -440,6 +443,24 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> { ...@@ -440,6 +443,24 @@ class RenderUiKitView extends RenderDarwinPlatformView<UiKitViewController> {
} }
} }
/// A render object for a macOS platform view.
class RenderAppKitView extends RenderDarwinPlatformView<AppKitViewController> {
/// Creates a render object for a macOS AppKitView.
RenderAppKitView({
required super.viewController,
required super.hitTestBehavior,
required super.gestureRecognizers,
});
// TODO(schectman): Add gesture functionality to macOS platform view when implemented.
// https://github.com/flutter/flutter/issues/128519
// This method will need to behave the same as the same-named method for RenderUiKitView,
// but use a _AppKitViewGestureRecognizer or equivalent, whose constructor shall accept an
// AppKitViewController.
@override
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {}
}
// This recognizer constructs gesture recognizers from a set of gesture recognizer factories // This recognizer constructs gesture recognizers from a set of gesture recognizer factories
// it was give, adds all of them to a gesture arena team with the _UiKitViewGestureRecognizer // it was give, adds all of them to a gesture arena team with the _UiKitViewGestureRecognizer
// as the team captain. // as the team captain.
......
...@@ -220,6 +220,7 @@ class PlatformViewsService { ...@@ -220,6 +220,7 @@ class PlatformViewsService {
assert(creationParams == null || creationParamsCodec != null); assert(creationParams == null || creationParamsCodec != null);
// TODO(amirh): pass layoutDirection once the system channel supports it. // TODO(amirh): pass layoutDirection once the system channel supports it.
// https://github.com/flutter/flutter/issues/133682
final Map<String, dynamic> args = <String, dynamic>{ final Map<String, dynamic> args = <String, dynamic>{
'id': id, 'id': id,
'viewType': viewType, 'viewType': viewType,
...@@ -238,6 +239,49 @@ class PlatformViewsService { ...@@ -238,6 +239,49 @@ class PlatformViewsService {
} }
return UiKitViewController._(id, layoutDirection); return UiKitViewController._(id, layoutDirection);
} }
/// Factory method to create an `AppKitView`.
///
/// `id` is an unused unique identifier generated with [platformViewsRegistry].
///
/// `viewType` is the identifier of the iOS view type to be created, a
/// factory for this view type must have been registered on the platform side.
/// Platform view factories are typically registered by plugin code.
///
/// `onFocus` is a callback that will be invoked when the UIKit view asks to
/// get the input focus.
/// The `id, `viewType, and `layoutDirection` parameters must not be null.
/// If `creationParams` is non null then `creationParamsCodec` must not be null.
static Future<AppKitViewController> initAppKitView({
required int id,
required String viewType,
required TextDirection layoutDirection,
dynamic creationParams,
MessageCodec<dynamic>? creationParamsCodec,
VoidCallback? onFocus,
}) async {
assert(creationParams == null || creationParamsCodec != null);
// TODO(amirh): pass layoutDirection once the system channel supports it.
// https://github.com/flutter/flutter/issues/133682
final Map<String, dynamic> args = <String, dynamic>{
'id': id,
'viewType': viewType,
};
if (creationParams != null) {
final ByteData paramsByteData = creationParamsCodec!.encodeMessage(creationParams)!;
args['params'] = Uint8List.view(
paramsByteData.buffer,
0,
paramsByteData.lengthInBytes,
);
}
await SystemChannels.platform_views.invokeMethod<void>('create', args);
if (onFocus != null) {
_instance._focusCallbacks[id] = onFocus;
}
return AppKitViewController._(id, layoutDirection);
}
} }
/// Properties of an Android pointer. /// Properties of an Android pointer.
...@@ -1317,7 +1361,6 @@ abstract class DarwinPlatformViewController { ...@@ -1317,7 +1361,6 @@ abstract class DarwinPlatformViewController {
TextDirection layoutDirection, TextDirection layoutDirection,
) : _layoutDirection = layoutDirection; ) : _layoutDirection = layoutDirection;
/// The unique identifier of the iOS view controlled by this controller. /// The unique identifier of the iOS view controlled by this controller.
/// ///
/// This identifier is typically generated by /// This identifier is typically generated by
...@@ -1389,6 +1432,14 @@ class UiKitViewController extends DarwinPlatformViewController { ...@@ -1389,6 +1432,14 @@ class UiKitViewController extends DarwinPlatformViewController {
); );
} }
/// Controller for a macOS platform view.
class AppKitViewController extends DarwinPlatformViewController {
AppKitViewController._(
super.id,
super.layoutDirection,
);
}
/// An interface for controlling a single platform view. /// An interface for controlling a single platform view.
/// ///
/// Used by [PlatformViewSurface] to interface with the platform view it embeds. /// Used by [PlatformViewSurface] to interface with the platform view it embeds.
......
...@@ -325,6 +325,40 @@ class UiKitView extends _DarwinView { ...@@ -325,6 +325,40 @@ class UiKitView extends _DarwinView {
State<UiKitView> createState() => _UiKitViewState(); State<UiKitView> createState() => _UiKitViewState();
} }
/// Widget that contains a macOS AppKit view.
///
/// Embedding macOS views is an expensive operation and should be avoided where
/// a Flutter equivalent is possible.
///
/// The platform view's lifetime is the same as the lifetime of the [State]
/// object for this widget. When the [State] is disposed the platform view (and
/// auxiliary resources) are lazily released (some resources are immediately
/// released and some by platform garbage collector). A stateful widget's state
/// is disposed when the widget is removed from the tree or when it is moved
/// within the tree. If the stateful widget has a key and it's only moved
/// relative to its siblings, or it has a [GlobalKey] and it's moved within the
/// tree, it will not be disposed.
///
/// Construction of AppKitViews is done asynchronously, before the underlying
/// NSView is ready this widget paints nothing while maintaining the same
/// layout constraints.
class AppKitView extends _DarwinView {
/// Creates a widget that embeds a macOS AppKit NSView.
const AppKitView({
super.key,
required super.viewType,
super.onPlatformViewCreated,
super.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
super.layoutDirection,
super.creationParams,
super.creationParamsCodec,
super.gestureRecognizers,
});
@override
State<AppKitView> createState() => _AppKitViewState();
}
/// Callback signature for when the platform view's DOM element was created. /// Callback signature for when the platform view's DOM element was created.
/// ///
/// [element] is the DOM element that was created. /// [element] is the DOM element that was created.
...@@ -711,6 +745,31 @@ class _UiKitViewState extends _DarwinViewState<UiKitView, UiKitViewController, R ...@@ -711,6 +745,31 @@ class _UiKitViewState extends _DarwinViewState<UiKitView, UiKitViewController, R
} }
} }
class _AppKitViewState extends _DarwinViewState<AppKitView, AppKitViewController, RenderAppKitView, _AppKitPlatformView> {
@override
Future<AppKitViewController> createNewViewController(int id) async {
return PlatformViewsService.initAppKitView(
id: id,
viewType: widget.viewType,
layoutDirection: _layoutDirection!,
creationParams: widget.creationParams,
creationParamsCodec: widget.creationParamsCodec,
onFocus: () {
focusNode?.requestFocus();
}
);
}
@override
_AppKitPlatformView childPlatformView() {
return _AppKitPlatformView(
controller: _controller!,
hitTestBehavior: widget.hitTestBehavior,
gestureRecognizers: widget.gestureRecognizers ?? _DarwinViewState._emptyRecognizersSet,
);
}
}
class _AndroidPlatformView extends LeafRenderObjectWidget { class _AndroidPlatformView extends LeafRenderObjectWidget {
const _AndroidPlatformView({ const _AndroidPlatformView({
required this.controller, required this.controller,
...@@ -758,7 +817,8 @@ abstract class _DarwinPlatformView<TController extends DarwinPlatformViewControl ...@@ -758,7 +817,8 @@ abstract class _DarwinPlatformView<TController extends DarwinPlatformViewControl
void updateRenderObject(BuildContext context, TRender renderObject) { void updateRenderObject(BuildContext context, TRender renderObject) {
renderObject renderObject
..viewController = controller ..viewController = controller
..hitTestBehavior = hitTestBehavior; ..hitTestBehavior = hitTestBehavior
..updateGestureRecognizers(gestureRecognizers);
} }
} }
...@@ -773,11 +833,18 @@ class _UiKitPlatformView extends _DarwinPlatformView<UiKitViewController, Render ...@@ -773,11 +833,18 @@ class _UiKitPlatformView extends _DarwinPlatformView<UiKitViewController, Render
gestureRecognizers: gestureRecognizers, gestureRecognizers: gestureRecognizers,
); );
} }
}
class _AppKitPlatformView extends _DarwinPlatformView<AppKitViewController, RenderAppKitView> {
const _AppKitPlatformView({required super.controller, required super.hitTestBehavior, required super.gestureRecognizers});
@override @override
void updateRenderObject(BuildContext context, RenderUiKitView renderObject) { RenderObject createRenderObject(BuildContext context) {
super.updateRenderObject(context, renderObject); return RenderAppKitView(
renderObject.updateGestureRecognizers(gestureRecognizers); viewController: controller,
hitTestBehavior: hitTestBehavior,
gestureRecognizers: gestureRecognizers,
);
} }
} }
......
...@@ -471,6 +471,109 @@ class FakeIosPlatformViewsController { ...@@ -471,6 +471,109 @@ class FakeIosPlatformViewsController {
} }
} }
class FakeMacosPlatformViewsController {
FakeMacosPlatformViewsController() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall);
}
Iterable<FakeAppKitView> get views => _views.values;
final Map<int, FakeAppKitView> _views = <int, FakeAppKitView>{};
final Set<String> _registeredViewTypes = <String>{};
// When this completer is non null, the 'create' method channel call will be
// delayed until it completes.
Completer<void>? creationDelay;
// Maps a view id to the number of gestures it accepted so far.
final Map<int, int> gesturesAccepted = <int, int>{};
// Maps a view id to the number of gestures it rejected so far.
final Map<int, int> gesturesRejected = <int, int>{};
void registerViewType(String viewType) {
_registeredViewTypes.add(viewType);
}
void invokeViewFocused(int viewId) {
final MethodCodec codec = SystemChannels.platform_views.codec;
final ByteData data = codec.encodeMethodCall(MethodCall('viewFocused', viewId));
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.handlePlatformMessage(SystemChannels.platform_views.name, data, (ByteData? data) {});
}
Future<dynamic> _onMethodCall(MethodCall call) {
switch (call.method) {
case 'create':
return _create(call);
case 'dispose':
return _dispose(call);
case 'acceptGesture':
return _acceptGesture(call);
case 'rejectGesture':
return _rejectGesture(call);
}
return Future<dynamic>.sync(() => null);
}
Future<dynamic> _create(MethodCall call) async {
if (creationDelay != null) {
await creationDelay!.future;
}
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 Uint8List? creationParams = args['params'] as Uint8List?;
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',
);
}
_views[id] = FakeAppKitView(id, viewType, creationParams);
gesturesAccepted[id] = 0;
gesturesRejected[id] = 0;
return Future<int?>.sync(() => null);
}
Future<dynamic> _acceptGesture(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
final int id = args['id'] as int;
gesturesAccepted[id] = gesturesAccepted[id]! + 1;
return Future<int?>.sync(() => null);
}
Future<dynamic> _rejectGesture(MethodCall call) async {
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
final int id = args['id'] as int;
gesturesRejected[id] = gesturesRejected[id]! + 1;
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 @immutable
class FakeAndroidPlatformView { class FakeAndroidPlatformView {
const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection,
...@@ -585,3 +688,31 @@ class FakeUiKitView { ...@@ -585,3 +688,31 @@ class FakeUiKitView {
return 'FakeUiKitView(id: $id, type: $type, creationParams: $creationParams)'; return 'FakeUiKitView(id: $id, type: $type, creationParams: $creationParams)';
} }
} }
@immutable
class FakeAppKitView {
const FakeAppKitView(this.id, this.type, [this.creationParams]);
final int id;
final String type;
final Uint8List? creationParams;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FakeAppKitView
&& other.id == id
&& other.type == type
&& other.creationParams == creationParams;
}
@override
int get hashCode => Object.hash(id, type);
@override
String toString() {
return 'FakeAppKitView(id: $id, type: $type, creationParams: $creationParams)';
}
}
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