8 #include <Carbon/Carbon.h>
9 #import <objc/message.h>
11 #include "flutter/common/constants.h"
12 #include "flutter/fml/platform/darwin/cf_utils.h"
24 #include "flutter/shell/platform/embedder/embedder.h"
26 #pragma mark - Static types and data.
32 static constexpr int32_t kMousePointerDeviceId = 0;
33 static constexpr int32_t kPointerPanZoomDeviceId = 1;
38 static constexpr
double kTrackpadTouchInertiaCancelWindowMs = 0.050;
71 bool flutter_state_is_added =
false;
76 bool flutter_state_is_down =
false;
85 bool has_pending_exit =
false;
90 bool flutter_state_is_pan_zoom_started =
false;
95 NSEventPhase pan_gesture_phase = NSEventPhaseNone;
100 NSEventPhase scale_gesture_phase = NSEventPhaseNone;
105 NSEventPhase rotate_gesture_phase = NSEventPhaseNone;
110 NSTimeInterval last_scroll_momentum_changed_time = 0;
115 void GestureReset() {
120 flutter_state_is_pan_zoom_started =
false;
121 pan_gesture_phase = NSEventPhaseNone;
122 scale_gesture_phase = NSEventPhaseNone;
123 rotate_gesture_phase = NSEventPhaseNone;
130 flutter_state_is_added =
false;
131 flutter_state_is_down =
false;
132 has_pending_exit =
false;
139 #pragma mark - Private interface declaration.
156 - (void)setBackgroundColor:(NSColor*)color;
168 @property(nonatomic) NSTrackingArea* trackingArea;
173 @property(nonatomic) MouseState mouseState;
178 @property(nonatomic)
id keyUpMonitor;
183 - (BOOL)launchEngine;
189 - (void)configureTrackingArea;
196 - (void)dispatchMouseEvent:(nonnull NSEvent*)event;
201 - (void)dispatchGestureEvent:(nonnull NSEvent*)event;
206 - (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase;
210 #pragma mark - FlutterViewWrapper implementation.
217 - (instancetype)initWithFlutterView:(
FlutterView*)view
219 self = [
super initWithFrame:NSZeroRect];
223 view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
224 [
self addSubview:view];
229 - (void)setBackgroundColor:(NSColor*)color {
230 [_flutterView setBackgroundColor:color];
233 - (BOOL)performKeyEquivalent:(NSEvent*)event {
240 if (
self.window.firstResponder != _flutterView || [
_controller isDispatchingKeyEvent:event]) {
241 return [
super performKeyEquivalent:event];
243 [_flutterView keyDown:event];
247 - (NSArray*)accessibilityChildren {
248 return @[ _flutterView ];
253 - (void)mouseDown:(NSEvent*)event {
254 if (@available(macOS 13.3.1, *)) {
255 [
super mouseDown:event];
267 [
self.nextResponder mouseDown:event];
273 - (void)mouseUp:(NSEvent*)event {
274 if (@available(macOS 13.3.1, *)) {
275 [
super mouseUp:event];
287 [
self.nextResponder mouseUp:event];
293 #pragma mark - FlutterViewController implementation.
299 std::shared_ptr<flutter::AccessibilityBridgeMac>
_bridge;
303 @synthesize viewIdentifier = _viewIdentifier;
305 @dynamic accessibilityBridge;
311 if (_engine.textInputPlugin.currentViewController ==
self) {
312 return _engine.textInputPlugin;
324 project:controller->_project
325 allowHeadlessExecution:NO];
327 NSCAssert(controller.
engine == nil,
328 @"The FlutterViewController is unexpectedly attached to "
329 @"engine %@ before initialization.",
331 [engine addViewController:controller];
333 NSCAssert(controller.
engine != nil,
334 @"The FlutterViewController unexpectedly stays unattached after initialization. "
335 @"In unit tests, this is likely because either the FlutterViewController or "
336 @"the FlutterEngine is mocked. Please subclass these classes instead.",
338 controller->_mouseTrackingMode = kFlutterMouseTrackingModeInKeyWindow;
339 [controller notifySemanticsEnabledChanged];
342 - (instancetype)initWithCoder:(NSCoder*)coder {
343 self = [
super initWithCoder:coder];
344 NSAssert(
self,
@"Super init cannot be nil");
346 CommonInit(
self, nil);
350 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
351 self = [
super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
352 NSAssert(
self,
@"Super init cannot be nil");
354 CommonInit(
self, nil);
359 self = [
super initWithNibName:nil bundle:nil];
360 NSAssert(
self,
@"Super init cannot be nil");
363 CommonInit(
self, nil);
367 - (instancetype)initWithEngine:(nonnull
FlutterEngine*)engine
368 nibName:(nullable NSString*)nibName
369 bundle:(nullable NSBundle*)nibBundle {
370 NSAssert(engine != nil,
@"Engine is required");
372 self = [
super initWithNibName:nibName bundle:nibBundle];
374 CommonInit(
self, engine);
380 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
381 return [_engine.keyboardManager isDispatchingKeyEvent:event];
386 id<MTLDevice> device = _engine.renderer.device;
387 id<MTLCommandQueue> commandQueue = _engine.renderer.commandQueue;
388 if (!device || !commandQueue) {
389 NSLog(
@"Unable to create FlutterView; no MTLDevice or MTLCommandQueue available.");
392 flutterView = [
self createFlutterViewWithMTLDevice:device commandQueue:commandQueue];
393 if (_backgroundColor != nil) {
398 self.view = wrapperView;
399 _flutterView = flutterView;
402 - (void)viewDidLoad {
403 [
self configureTrackingArea];
404 [
self.view setAllowedTouchTypes:NSTouchTypeMaskIndirect];
405 [
self.view setWantsRestingTouches:YES];
406 [_engine viewControllerViewDidLoad:self];
409 - (void)viewWillAppear {
410 [
super viewWillAppear];
411 if (!_engine.running) {
414 [
self listenForMetaModifiedKeyUpEvents];
417 - (void)viewWillDisappear {
420 [NSEvent removeMonitor:_keyUpMonitor];
425 if ([
self attached]) {
426 [_engine removeViewController:self];
428 [
self.flutterView shutDown];
435 #pragma mark - Public methods
437 - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {
438 if (_mouseTrackingMode == mode) {
441 _mouseTrackingMode = mode;
442 [
self configureTrackingArea];
445 - (void)setBackgroundColor:(NSColor*)color {
446 _backgroundColor = color;
447 [_flutterView setBackgroundColor:_backgroundColor];
451 NSAssert([
self attached],
@"This view controller is not attached.");
452 return _viewIdentifier;
455 - (void)onPreEngineRestart {
458 - (void)notifySemanticsEnabledChanged {
459 BOOL mySemanticsEnabled = !!
_bridge;
460 BOOL newSemanticsEnabled = _engine.semanticsEnabled;
461 if (newSemanticsEnabled == mySemanticsEnabled) {
464 if (newSemanticsEnabled) {
465 _bridge = [
self createAccessibilityBridgeWithEngine:_engine];
468 _flutterView.accessibilityChildren = nil;
471 NSAssert(newSemanticsEnabled == !!
_bridge,
@"Failed to update semantics for the view.");
474 - (std::weak_ptr<flutter::AccessibilityBridgeMac>)accessibilityBridge {
480 NSAssert(_engine == nil,
@"Already attached to an engine %@.", _engine);
482 _viewIdentifier = viewIdentifier;
485 - (void)detachFromEngine {
486 NSAssert(_engine != nil,
@"Not attached to any engine.");
491 return _engine != nil;
494 - (void)updateSemantics:(const FlutterSemanticsUpdate2*)update {
497 if (!_engine.semanticsEnabled) {
500 for (
size_t i = 0; i < update->node_count; i++) {
501 const FlutterSemanticsNode2* node = update->nodes[i];
502 _bridge->AddFlutterSemanticsNodeUpdate(*node);
505 for (
size_t i = 0; i < update->custom_action_count; i++) {
506 const FlutterSemanticsCustomAction2* action = update->custom_actions[i];
507 _bridge->AddFlutterSemanticsCustomActionUpdate(*action);
513 if (!
self.viewLoaded) {
517 auto root =
_bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
519 if ([
self.flutterView.accessibilityChildren count] == 0) {
520 NSAccessibilityElement* native_root = root->GetNativeViewAccessible();
521 self.flutterView.accessibilityChildren = @[ native_root ];
524 self.flutterView.accessibilityChildren = nil;
528 #pragma mark - Private methods
530 - (BOOL)launchEngine {
531 if (![_engine runWithEntrypoint:nil]) {
541 - (void)listenForMetaModifiedKeyUpEvents {
542 if (_keyUpMonitor != nil) {
548 _keyUpMonitor = [NSEvent
549 addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
550 handler:^NSEvent*(NSEvent* event) {
553 NSResponder* firstResponder = [[event window] firstResponder];
554 if (weakSelf.viewLoaded && weakSelf.flutterView &&
555 (firstResponder == weakSelf.flutterView ||
556 firstResponder == weakSelf.activeTextInputPlugin) &&
557 ([event modifierFlags] & NSEventModifierFlagCommand) &&
558 ([event type] == NSEventTypeKeyUp)) {
559 [weakSelf keyUp:event];
565 - (void)configureTrackingArea {
566 if (!
self.viewLoaded) {
571 if (_mouseTrackingMode != kFlutterMouseTrackingModeNone &&
self.flutterView) {
572 NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
573 NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag;
574 switch (_mouseTrackingMode) {
575 case kFlutterMouseTrackingModeInKeyWindow:
576 options |= NSTrackingActiveInKeyWindow;
578 case kFlutterMouseTrackingModeInActiveApp:
579 options |= NSTrackingActiveInActiveApp;
581 case kFlutterMouseTrackingModeAlways:
582 options |= NSTrackingActiveAlways;
585 NSLog(
@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode);
588 _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
592 [
self.flutterView addTrackingArea:_trackingArea];
593 }
else if (_trackingArea) {
594 [
self.flutterView removeTrackingArea:_trackingArea];
599 - (void)dispatchMouseEvent:(nonnull NSEvent*)event {
600 FlutterPointerPhase phase = _mouseState.buttons == 0
601 ? (_mouseState.flutter_state_is_down ? kUp : kHover)
602 : (_mouseState.flutter_state_is_down ? kMove : kDown);
603 [
self dispatchMouseEvent:event phase:phase];
606 - (void)dispatchGestureEvent:(nonnull NSEvent*)event {
607 if (event.phase == NSEventPhaseBegan || event.phase == NSEventPhaseMayBegin) {
608 [
self dispatchMouseEvent:event phase:kPanZoomStart];
609 }
else if (event.phase == NSEventPhaseChanged) {
610 [
self dispatchMouseEvent:event phase:kPanZoomUpdate];
611 }
else if (event.phase == NSEventPhaseEnded || event.phase == NSEventPhaseCancelled) {
612 [
self dispatchMouseEvent:event phase:kPanZoomEnd];
613 }
else if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) {
614 [
self dispatchMouseEvent:event phase:kHover];
619 if (event.momentumPhase == NSEventPhaseChanged) {
620 _mouseState.last_scroll_momentum_changed_time =
event.timestamp;
623 NSAssert(event.momentumPhase != NSEventPhaseNone,
624 @"Received gesture event with unexpected phase");
628 - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
629 NSAssert(
self.viewLoaded,
@"View must be loaded before it handles the mouse event");
633 if (_mouseState.flutter_state_is_added && phase == kAdd) {
639 if (phase == kPanZoomStart || phase == kPanZoomEnd) {
640 if (event.type == NSEventTypeScrollWheel) {
641 _mouseState.pan_gesture_phase =
event.phase;
642 }
else if (event.type == NSEventTypeMagnify) {
643 _mouseState.scale_gesture_phase =
event.phase;
644 }
else if (event.type == NSEventTypeRotate) {
645 _mouseState.rotate_gesture_phase =
event.phase;
648 if (phase == kPanZoomStart) {
649 if (event.type == NSEventTypeScrollWheel) {
651 _mouseState.last_scroll_momentum_changed_time = 0;
653 if (_mouseState.flutter_state_is_pan_zoom_started) {
657 _mouseState.flutter_state_is_pan_zoom_started =
true;
659 if (phase == kPanZoomEnd) {
660 if (!_mouseState.flutter_state_is_pan_zoom_started) {
664 NSAssert(event.phase == NSEventPhaseCancelled,
665 @"Received gesture event with unexpected phase");
669 NSEventPhase all_gestures_fields = _mouseState.pan_gesture_phase |
670 _mouseState.scale_gesture_phase |
671 _mouseState.rotate_gesture_phase;
672 NSEventPhase active_mask = NSEventPhaseBegan | NSEventPhaseChanged;
673 if ((all_gestures_fields & active_mask) != 0) {
681 if (!_mouseState.flutter_state_is_added && phase != kAdd) {
683 NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
684 location:event.locationInWindow
686 timestamp:event.timestamp
687 windowNumber:event.windowNumber
692 [
self dispatchMouseEvent:addEvent phase:kAdd];
695 NSPoint locationInView = [
self.flutterView convertPoint:event.locationInWindow fromView:nil];
696 NSPoint locationInBackingCoordinates = [
self.flutterView convertPointToBacking:locationInView];
697 int32_t device = kMousePointerDeviceId;
698 FlutterPointerDeviceKind deviceKind = kFlutterPointerDeviceKindMouse;
699 if (phase == kPanZoomStart || phase == kPanZoomUpdate || phase == kPanZoomEnd) {
700 device = kPointerPanZoomDeviceId;
701 deviceKind = kFlutterPointerDeviceKindTrackpad;
703 FlutterPointerEvent flutterEvent = {
704 .struct_size =
sizeof(flutterEvent),
706 .timestamp =
static_cast<size_t>(event.timestamp * USEC_PER_SEC),
707 .x = locationInBackingCoordinates.x,
708 .y = -locationInBackingCoordinates.y,
710 .device_kind = deviceKind,
712 .buttons = phase == kAdd ? 0 : _mouseState.buttons,
713 .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
716 if (phase == kPanZoomUpdate) {
717 if (event.type == NSEventTypeScrollWheel) {
718 _mouseState.delta_x += event.scrollingDeltaX * self.flutterView.layer.contentsScale;
719 _mouseState.delta_y += event.scrollingDeltaY * self.flutterView.layer.contentsScale;
720 }
else if (event.type == NSEventTypeMagnify) {
721 _mouseState.scale += event.magnification;
722 }
else if (event.type == NSEventTypeRotate) {
723 _mouseState.rotation += event.rotation * (-M_PI / 180.0);
725 flutterEvent.pan_x = _mouseState.delta_x;
726 flutterEvent.pan_y = _mouseState.delta_y;
728 flutterEvent.scale = pow(2.0, _mouseState.scale);
729 flutterEvent.rotation = _mouseState.rotation;
730 }
else if (phase == kPanZoomEnd) {
731 _mouseState.GestureReset();
732 }
else if (phase != kPanZoomStart && event.type == NSEventTypeScrollWheel) {
733 flutterEvent.signal_kind = kFlutterPointerSignalKindScroll;
735 double pixelsPerLine = 1.0;
736 if (!event.hasPreciseScrollingDeltas) {
742 pixelsPerLine = 40.0;
744 double scaleFactor =
self.flutterView.layer.contentsScale;
753 double scaledDeltaX = -event.scrollingDeltaX * pixelsPerLine * scaleFactor;
754 double scaledDeltaY = -event.scrollingDeltaY * pixelsPerLine * scaleFactor;
755 if (event.modifierFlags & NSShiftKeyMask) {
756 flutterEvent.scroll_delta_x = scaledDeltaY;
757 flutterEvent.scroll_delta_y = scaledDeltaX;
759 flutterEvent.scroll_delta_x = scaledDeltaX;
760 flutterEvent.scroll_delta_y = scaledDeltaY;
764 [_engine.keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
765 [_engine sendPointerEvent:flutterEvent];
768 if (phase == kDown) {
769 _mouseState.flutter_state_is_down =
true;
770 }
else if (phase == kUp) {
771 _mouseState.flutter_state_is_down =
false;
772 if (_mouseState.has_pending_exit) {
773 [
self dispatchMouseEvent:event phase:kRemove];
774 _mouseState.has_pending_exit =
false;
776 }
else if (phase == kAdd) {
777 _mouseState.flutter_state_is_added =
true;
778 }
else if (phase == kRemove) {
783 - (void)onAccessibilityStatusChanged:(BOOL)enabled {
784 if (!enabled &&
self.viewLoaded && [
self.activeTextInputPlugin isFirstResponder]) {
789 [
self.view addSubview:self.activeTextInputPlugin];
793 - (std::shared_ptr<flutter::AccessibilityBridgeMac>)createAccessibilityBridgeWithEngine:
795 return std::make_shared<flutter::AccessibilityBridgeMac>(engine,
self);
798 - (nonnull
FlutterView*)createFlutterViewWithMTLDevice:(
id<MTLDevice>)device
799 commandQueue:(
id<MTLCommandQueue>)commandQueue {
801 return [[
FlutterView alloc] initWithMTLDevice:device
802 commandQueue:commandQueue
804 viewIdentifier:_viewIdentifier
805 enableWideGamut:project.enableWideGamut];
808 - (void)updateWideGamutForScreen {
810 if (!project.enableWideGamut) {
813 NSScreen* screen =
self.view.window.screen;
817 BOOL screenSupportsP3 = [screen canRepresentDisplayGamut:NSDisplayGamutP3];
818 [
self.flutterView setEnableWideGamut:screenSupportsP3];
821 - (NSString*)lookupKeyForAsset:(NSString*)asset {
825 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
829 #pragma mark - FlutterViewDelegate
834 - (void)viewDidReshape:(NSView*)view {
835 FML_DCHECK(view == _flutterView);
836 [_engine updateWindowMetricsForViewController:self];
839 - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view {
840 FML_DCHECK(view == _flutterView);
844 return !
self.activeTextInputPlugin.isFirstResponder;
847 #pragma mark - FlutterPluginRegistry
850 return [_engine registrarForPlugin:pluginName];
853 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
854 return [_engine valuePublishedByPlugin:pluginKey];
857 #pragma mark - FlutterKeyboardViewDelegate
859 - (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event {
860 return [
self.activeTextInputPlugin handleKeyEvent:event];
863 #pragma mark - NSResponder
865 - (BOOL)acceptsFirstResponder {
869 - (void)keyDown:(NSEvent*)event {
870 [_engine.keyboardManager handleEvent:event withContext:self];
873 - (void)keyUp:(NSEvent*)event {
874 [_engine.keyboardManager handleEvent:event withContext:self];
877 - (void)flagsChanged:(NSEvent*)event {
878 [_engine.keyboardManager handleEvent:event withContext:self];
881 - (void)mouseEntered:(NSEvent*)event {
882 if (_mouseState.has_pending_exit) {
883 _mouseState.has_pending_exit =
false;
885 [
self dispatchMouseEvent:event phase:kAdd];
889 - (void)mouseExited:(NSEvent*)event {
890 if (_mouseState.buttons != 0) {
891 _mouseState.has_pending_exit =
true;
894 [
self dispatchMouseEvent:event phase:kRemove];
897 - (void)mouseDown:(NSEvent*)event {
898 _mouseState.buttons |= kFlutterPointerButtonMousePrimary;
899 [
self dispatchMouseEvent:event];
902 - (void)mouseUp:(NSEvent*)event {
903 _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary);
904 [
self dispatchMouseEvent:event];
907 - (void)mouseDragged:(NSEvent*)event {
908 [
self dispatchMouseEvent:event];
911 - (void)rightMouseDown:(NSEvent*)event {
912 _mouseState.buttons |= kFlutterPointerButtonMouseSecondary;
913 [
self dispatchMouseEvent:event];
916 - (void)rightMouseUp:(NSEvent*)event {
917 _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary);
918 [
self dispatchMouseEvent:event];
921 - (void)rightMouseDragged:(NSEvent*)event {
922 [
self dispatchMouseEvent:event];
925 - (void)otherMouseDown:(NSEvent*)event {
926 _mouseState.buttons |= (1 <<
event.buttonNumber);
927 [
self dispatchMouseEvent:event];
930 - (void)otherMouseUp:(NSEvent*)event {
931 _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber);
932 [
self dispatchMouseEvent:event];
935 - (void)otherMouseDragged:(NSEvent*)event {
936 [
self dispatchMouseEvent:event];
939 - (void)mouseMoved:(NSEvent*)event {
940 [
self dispatchMouseEvent:event];
943 - (void)scrollWheel:(NSEvent*)event {
944 [
self dispatchGestureEvent:event];
947 - (void)magnifyWithEvent:(NSEvent*)event {
948 [
self dispatchGestureEvent:event];
951 - (void)rotateWithEvent:(NSEvent*)event {
952 [
self dispatchGestureEvent:event];
955 - (void)swipeWithEvent:(NSEvent*)event {
959 - (void)touchesBeganWithEvent:(NSEvent*)event {
960 NSTouch* touch =
event.allTouches.anyObject;
962 if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
963 kTrackpadTouchInertiaCancelWindowMs) {
966 NSPoint locationInView = [
self.flutterView convertPoint:event.locationInWindow fromView:nil];
967 NSPoint locationInBackingCoordinates =
968 [
self.flutterView convertPointToBacking:locationInView];
969 FlutterPointerEvent flutterEvent = {
970 .struct_size =
sizeof(flutterEvent),
971 .timestamp =
static_cast<size_t>(event.timestamp * USEC_PER_SEC),
972 .x = locationInBackingCoordinates.x,
973 .y = -locationInBackingCoordinates.y,
974 .device = kPointerPanZoomDeviceId,
975 .signal_kind = kFlutterPointerSignalKindScrollInertiaCancel,
976 .device_kind = kFlutterPointerDeviceKindTrackpad,
980 [_engine sendPointerEvent:flutterEvent];
982 _mouseState.last_scroll_momentum_changed_time = 0;
FlutterDartProject * _project
int64_t FlutterViewIdentifier
__weak FlutterViewController * _controller
std::shared_ptr< flutter::AccessibilityBridgeMac > _bridge
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
NSString * lookupKeyForAsset:(NSString *asset)
FlutterViewIdentifier viewIdentifier
void setBackgroundColor:(nonnull NSColor *color)