Flutter macOS Embedder
FlutterViewController.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
7 
8 #include <Carbon/Carbon.h>
9 #import <objc/message.h>
10 
11 #include "flutter/common/constants.h"
12 #include "flutter/fml/platform/darwin/cf_utils.h"
24 #include "flutter/shell/platform/embedder/embedder.h"
25 
26 #pragma mark - Static types and data.
27 
28 namespace {
29 
30 // Use different device ID for mouse and pan/zoom events, since we can't differentiate the actual
31 // device (mouse v.s. trackpad).
32 static constexpr int32_t kMousePointerDeviceId = 0;
33 static constexpr int32_t kPointerPanZoomDeviceId = 1;
34 
35 // A trackpad touch following inertial scrolling should cause an inertia cancel
36 // event to be issued. Use a window of 50 milliseconds after the scroll to account
37 // for delays in event propagation observed in macOS Ventura.
38 static constexpr double kTrackpadTouchInertiaCancelWindowMs = 0.050;
39 
40 /**
41  * State tracking for mouse events, to adapt between the events coming from the system and the
42  * events that the embedding API expects.
43  */
44 struct MouseState {
45  /**
46  * The currently pressed buttons, as represented in FlutterPointerEvent.
47  */
48  int64_t buttons = 0;
49 
50  /**
51  * The accumulated gesture pan.
52  */
53  CGFloat delta_x = 0;
54  CGFloat delta_y = 0;
55 
56  /**
57  * The accumulated gesture zoom scale.
58  */
59  CGFloat scale = 0;
60 
61  /**
62  * The accumulated gesture rotation.
63  */
64  CGFloat rotation = 0;
65 
66  /**
67  * Whether or not a kAdd event has been sent (or sent again since the last kRemove if tracking is
68  * enabled). Used to determine whether to send a kAdd event before sending an incoming mouse
69  * event, since Flutter expects pointers to be added before events are sent for them.
70  */
71  bool flutter_state_is_added = false;
72 
73  /**
74  * Whether or not a kDown has been sent since the last kAdd/kUp.
75  */
76  bool flutter_state_is_down = false;
77 
78  /**
79  * Whether or not mouseExited: was received while a button was down. Cocoa's behavior when
80  * dragging out of a tracked area is to send an exit, then keep sending drag events until the last
81  * button is released. Flutter doesn't expect to receive events after a kRemove, so the kRemove
82  * for the exit needs to be delayed until after the last mouse button is released. If cursor
83  * returns back to the window while still dragging, the flag is cleared in mouseEntered:.
84  */
85  bool has_pending_exit = false;
86 
87  /*
88  * Whether or not a kPanZoomStart has been sent since the last kAdd/kPanZoomEnd.
89  */
90  bool flutter_state_is_pan_zoom_started = false;
91 
92  /**
93  * State of pan gesture.
94  */
95  NSEventPhase pan_gesture_phase = NSEventPhaseNone;
96 
97  /**
98  * State of scale gesture.
99  */
100  NSEventPhase scale_gesture_phase = NSEventPhaseNone;
101 
102  /**
103  * State of rotate gesture.
104  */
105  NSEventPhase rotate_gesture_phase = NSEventPhaseNone;
106 
107  /**
108  * Time of last scroll momentum event.
109  */
110  NSTimeInterval last_scroll_momentum_changed_time = 0;
111 
112  /**
113  * Resets all gesture state to default values.
114  */
115  void GestureReset() {
116  delta_x = 0;
117  delta_y = 0;
118  scale = 0;
119  rotation = 0;
120  flutter_state_is_pan_zoom_started = false;
121  pan_gesture_phase = NSEventPhaseNone;
122  scale_gesture_phase = NSEventPhaseNone;
123  rotate_gesture_phase = NSEventPhaseNone;
124  }
125 
126  /**
127  * Resets all state to default values.
128  */
129  void Reset() {
130  flutter_state_is_added = false;
131  flutter_state_is_down = false;
132  has_pending_exit = false;
133  buttons = 0;
134  }
135 };
136 
137 } // namespace
138 
139 #pragma mark - Private interface declaration.
140 
141 /**
142  * FlutterViewWrapper is a convenience class that wraps a FlutterView and provides
143  * a mechanism to attach AppKit views such as FlutterTextField without affecting
144  * the accessibility subtree of the wrapped FlutterView itself.
145  *
146  * The FlutterViewController uses this class to create its content view. When
147  * any of the accessibility services (e.g. VoiceOver) is turned on, the accessibility
148  * bridge creates FlutterTextFields that interact with the service. The bridge has to
149  * attach the FlutterTextField somewhere in the view hierarchy in order for the
150  * FlutterTextField to interact correctly with VoiceOver. Those FlutterTextFields
151  * will be attached to this view so that they won't affect the accessibility subtree
152  * of FlutterView.
153  */
154 @interface FlutterViewWrapper : NSView
155 
156 - (void)setBackgroundColor:(NSColor*)color;
157 
158 @end
159 
160 /**
161  * Private interface declaration for FlutterViewController.
162  */
164 
165 /**
166  * The tracking area used to generate hover events, if enabled.
167  */
168 @property(nonatomic) NSTrackingArea* trackingArea;
169 
170 /**
171  * The current state of the mouse and the sent mouse events.
172  */
173 @property(nonatomic) MouseState mouseState;
174 
175 /**
176  * Event monitor for keyUp events.
177  */
178 @property(nonatomic) id keyUpMonitor;
179 
180 /**
181  * Starts running |engine|, including any initial setup.
182  */
183 - (BOOL)launchEngine;
184 
185 /**
186  * Updates |trackingArea| for the current tracking settings, creating it with
187  * the correct mode if tracking is enabled, or removing it if not.
188  */
189 - (void)configureTrackingArea;
190 
191 /**
192  * Calls dispatchMouseEvent:phase: with a phase determined by self.mouseState.
193  *
194  * mouseState.buttons should be updated before calling this method.
195  */
196 - (void)dispatchMouseEvent:(nonnull NSEvent*)event;
197 
198 /**
199  * Calls dispatchMouseEvent:phase: with a phase determined by event.phase.
200  */
201 - (void)dispatchGestureEvent:(nonnull NSEvent*)event;
202 
203 /**
204  * Converts |event| to a FlutterPointerEvent with the given phase, and sends it to the engine.
205  */
206 - (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase;
207 
208 @end
209 
210 #pragma mark - FlutterViewWrapper implementation.
211 
212 @implementation FlutterViewWrapper {
213  FlutterView* _flutterView;
215 }
216 
217 - (instancetype)initWithFlutterView:(FlutterView*)view
218  controller:(FlutterViewController*)controller {
219  self = [super initWithFrame:NSZeroRect];
220  if (self) {
221  _flutterView = view;
222  _controller = controller;
223  view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
224  [self addSubview:view];
225  }
226  return self;
227 }
228 
229 - (void)setBackgroundColor:(NSColor*)color {
230  [_flutterView setBackgroundColor:color];
231 }
232 
233 - (BOOL)performKeyEquivalent:(NSEvent*)event {
234  // Do not intercept the event if flutterView is not first responder, otherwise this would
235  // interfere with TextInputPlugin, which also handles key equivalents.
236  //
237  // Also do not intercept the event if key equivalent is a product of an event being
238  // redispatched by the TextInputPlugin, in which case it needs to bubble up so that menus
239  // can handle key equivalents.
240  if (self.window.firstResponder != _flutterView || [_controller isDispatchingKeyEvent:event]) {
241  return [super performKeyEquivalent:event];
242  }
243  [_flutterView keyDown:event];
244  return YES;
245 }
246 
247 - (NSArray*)accessibilityChildren {
248  return @[ _flutterView ];
249 }
250 
251 // TODO(cbracken): https://github.com/flutter/flutter/issues/154063
252 // Remove this whole method override when we drop support for macOS 12 (Monterey).
253 - (void)mouseDown:(NSEvent*)event {
254  if (@available(macOS 13.3.1, *)) {
255  [super mouseDown:event];
256  } else {
257  // Work around an AppKit bug where mouseDown/mouseUp are not called on the view controller if
258  // the view is the content view of an NSPopover AND macOS's Reduced Transparency accessibility
259  // setting is enabled.
260  //
261  // This simply calls mouseDown on the next responder in the responder chain as the default
262  // implementation on NSResponder is documented to do.
263  //
264  // See: https://github.com/flutter/flutter/issues/115015
265  // See: http://www.openradar.me/FB12050037
266  // See: https://developer.apple.com/documentation/appkit/nsresponder/1524634-mousedown
267  [self.nextResponder mouseDown:event];
268  }
269 }
270 
271 // TODO(cbracken): https://github.com/flutter/flutter/issues/154063
272 // Remove this workaround when we drop support for macOS 12 (Monterey).
273 - (void)mouseUp:(NSEvent*)event {
274  if (@available(macOS 13.3.1, *)) {
275  [super mouseUp:event];
276  } else {
277  // Work around an AppKit bug where mouseDown/mouseUp are not called on the view controller if
278  // the view is the content view of an NSPopover AND macOS's Reduced Transparency accessibility
279  // setting is enabled.
280  //
281  // This simply calls mouseUp on the next responder in the responder chain as the default
282  // implementation on NSResponder is documented to do.
283  //
284  // See: https://github.com/flutter/flutter/issues/115015
285  // See: http://www.openradar.me/FB12050037
286  // See: https://developer.apple.com/documentation/appkit/nsresponder/1535349-mouseup
287  [self.nextResponder mouseUp:event];
288  }
289 }
290 
291 @end
292 
293 #pragma mark - FlutterViewController implementation.
294 
295 @implementation FlutterViewController {
296  // The project to run in this controller's engine.
298 
299  std::shared_ptr<flutter::AccessibilityBridgeMac> _bridge;
300 }
301 
302 // Synthesize properties declared readonly.
303 @synthesize viewIdentifier = _viewIdentifier;
304 
305 @dynamic accessibilityBridge;
306 
307 // Returns the text input plugin associated with this view controller.
308 // This method only returns non nil instance if the text input plugin has active
309 // client with viewId matching this controller's view identifier.
310 - (FlutterTextInputPlugin*)activeTextInputPlugin {
311  if (_engine.textInputPlugin.currentViewController == self) {
312  return _engine.textInputPlugin;
313  } else {
314  return nil;
315  }
316 }
317 
318 /**
319  * Performs initialization that's common between the different init paths.
320  */
321 static void CommonInit(FlutterViewController* controller, FlutterEngine* engine) {
322  if (!engine) {
323  engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
324  project:controller->_project
325  allowHeadlessExecution:NO];
326  }
327  NSCAssert(controller.engine == nil,
328  @"The FlutterViewController is unexpectedly attached to "
329  @"engine %@ before initialization.",
330  controller.engine);
331  [engine addViewController:controller];
332 
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.",
337  controller.engine, controller.viewIdentifier);
338  controller->_mouseTrackingMode = kFlutterMouseTrackingModeInKeyWindow;
339  [controller notifySemanticsEnabledChanged];
340 }
341 
342 - (instancetype)initWithCoder:(NSCoder*)coder {
343  self = [super initWithCoder:coder];
344  NSAssert(self, @"Super init cannot be nil");
345 
346  CommonInit(self, nil);
347  return self;
348 }
349 
350 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
351  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
352  NSAssert(self, @"Super init cannot be nil");
353 
354  CommonInit(self, nil);
355  return self;
356 }
357 
358 - (instancetype)initWithProject:(nullable FlutterDartProject*)project {
359  self = [super initWithNibName:nil bundle:nil];
360  NSAssert(self, @"Super init cannot be nil");
361 
362  _project = project;
363  CommonInit(self, nil);
364  return self;
365 }
366 
367 - (instancetype)initWithEngine:(nonnull FlutterEngine*)engine
368  nibName:(nullable NSString*)nibName
369  bundle:(nullable NSBundle*)nibBundle {
370  NSAssert(engine != nil, @"Engine is required");
371 
372  self = [super initWithNibName:nibName bundle:nibBundle];
373  if (self) {
374  CommonInit(self, engine);
375  }
376 
377  return self;
378 }
379 
380 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
381  return [_engine.keyboardManager isDispatchingKeyEvent:event];
382 }
383 
384 - (void)loadView {
385  FlutterView* flutterView;
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.");
390  return;
391  }
392  flutterView = [self createFlutterViewWithMTLDevice:device commandQueue:commandQueue];
393  if (_backgroundColor != nil) {
394  [flutterView setBackgroundColor:_backgroundColor];
395  }
396  FlutterViewWrapper* wrapperView = [[FlutterViewWrapper alloc] initWithFlutterView:flutterView
397  controller:self];
398  self.view = wrapperView;
399  _flutterView = flutterView;
400 }
401 
402 - (void)viewDidLoad {
403  [self configureTrackingArea];
404  [self.view setAllowedTouchTypes:NSTouchTypeMaskIndirect];
405  [self.view setWantsRestingTouches:YES];
406  [_engine viewControllerViewDidLoad:self];
407 }
408 
409 - (void)viewWillAppear {
410  [super viewWillAppear];
411  if (!_engine.running) {
412  [self launchEngine];
413  }
414  [self listenForMetaModifiedKeyUpEvents];
415 }
416 
417 - (void)viewWillDisappear {
418  // Per Apple's documentation, it is discouraged to call removeMonitor: in dealloc, and it's
419  // recommended to be called earlier in the lifecycle.
420  [NSEvent removeMonitor:_keyUpMonitor];
421  _keyUpMonitor = nil;
422 }
423 
424 - (void)dispose {
425  if ([self attached]) {
426  [_engine removeViewController:self];
427  }
428  [self.flutterView shutDown];
429 }
430 
431 - (void)dealloc {
432  [self dispose];
433 }
434 
435 #pragma mark - Public methods
436 
437 - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {
438  if (_mouseTrackingMode == mode) {
439  return;
440  }
441  _mouseTrackingMode = mode;
442  [self configureTrackingArea];
443 }
444 
445 - (void)setBackgroundColor:(NSColor*)color {
446  _backgroundColor = color;
447  [_flutterView setBackgroundColor:_backgroundColor];
448 }
449 
450 - (FlutterViewIdentifier)viewIdentifier {
451  NSAssert([self attached], @"This view controller is not attached.");
452  return _viewIdentifier;
453 }
454 
455 - (void)onPreEngineRestart {
456 }
457 
458 - (void)notifySemanticsEnabledChanged {
459  BOOL mySemanticsEnabled = !!_bridge;
460  BOOL newSemanticsEnabled = _engine.semanticsEnabled;
461  if (newSemanticsEnabled == mySemanticsEnabled) {
462  return;
463  }
464  if (newSemanticsEnabled) {
465  _bridge = [self createAccessibilityBridgeWithEngine:_engine];
466  } else {
467  // Remove the accessibility children from flutter view before resetting the bridge.
468  _flutterView.accessibilityChildren = nil;
469  _bridge.reset();
470  }
471  NSAssert(newSemanticsEnabled == !!_bridge, @"Failed to update semantics for the view.");
472 }
473 
474 - (std::weak_ptr<flutter::AccessibilityBridgeMac>)accessibilityBridge {
475  return _bridge;
476 }
477 
478 - (void)setUpWithEngine:(FlutterEngine*)engine
479  viewIdentifier:(FlutterViewIdentifier)viewIdentifier {
480  NSAssert(_engine == nil, @"Already attached to an engine %@.", _engine);
481  _engine = engine;
482  _viewIdentifier = viewIdentifier;
483 }
484 
485 - (void)detachFromEngine {
486  NSAssert(_engine != nil, @"Not attached to any engine.");
487  _engine = nil;
488 }
489 
490 - (BOOL)attached {
491  return _engine != nil;
492 }
493 
494 - (void)updateSemantics:(const FlutterSemanticsUpdate2*)update {
495  // Semantics will be disabled when unfocusing application but the updateSemantics:
496  // callback is received in next run loop turn.
497  if (!_engine.semanticsEnabled) {
498  return;
499  }
500  for (size_t i = 0; i < update->node_count; i++) {
501  const FlutterSemanticsNode2* node = update->nodes[i];
502  _bridge->AddFlutterSemanticsNodeUpdate(*node);
503  }
504 
505  for (size_t i = 0; i < update->custom_action_count; i++) {
506  const FlutterSemanticsCustomAction2* action = update->custom_actions[i];
507  _bridge->AddFlutterSemanticsCustomActionUpdate(*action);
508  }
509 
510  _bridge->CommitUpdates();
511 
512  // Accessibility tree can only be used when the view is loaded.
513  if (!self.viewLoaded) {
514  return;
515  }
516  // Attaches the accessibility root to the flutter view.
517  auto root = _bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
518  if (root) {
519  if ([self.flutterView.accessibilityChildren count] == 0) {
520  NSAccessibilityElement* native_root = root->GetNativeViewAccessible();
521  self.flutterView.accessibilityChildren = @[ native_root ];
522  }
523  } else {
524  self.flutterView.accessibilityChildren = nil;
525  }
526 }
527 
528 #pragma mark - Private methods
529 
530 - (BOOL)launchEngine {
531  if (![_engine runWithEntrypoint:nil]) {
532  return NO;
533  }
534  return YES;
535 }
536 
537 // macOS does not call keyUp: on a key while the command key is pressed. This results in a loss
538 // of a key event once the modified key is released. This method registers the
539 // ViewController as a listener for a keyUp event before it's handled by NSApplication, and should
540 // NOT modify the event to avoid any unexpected behavior.
541 - (void)listenForMetaModifiedKeyUpEvents {
542  if (_keyUpMonitor != nil) {
543  // It is possible for [NSViewController viewWillAppear] to be invoked multiple times
544  // in a row. https://github.com/flutter/flutter/issues/105963
545  return;
546  }
547  FlutterViewController* __weak weakSelf = self;
548  _keyUpMonitor = [NSEvent
549  addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
550  handler:^NSEvent*(NSEvent* event) {
551  // Intercept keyUp only for events triggered on the current
552  // view or textInputPlugin.
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];
560  }
561  return event;
562  }];
563 }
564 
565 - (void)configureTrackingArea {
566  if (!self.viewLoaded) {
567  // The viewDidLoad will call configureTrackingArea again when
568  // the view is actually loaded.
569  return;
570  }
571  if (_mouseTrackingMode != kFlutterMouseTrackingModeNone && self.flutterView) {
572  NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
573  NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag;
574  switch (_mouseTrackingMode) {
575  case kFlutterMouseTrackingModeInKeyWindow:
576  options |= NSTrackingActiveInKeyWindow;
577  break;
578  case kFlutterMouseTrackingModeInActiveApp:
579  options |= NSTrackingActiveInActiveApp;
580  break;
581  case kFlutterMouseTrackingModeAlways:
582  options |= NSTrackingActiveAlways;
583  break;
584  default:
585  NSLog(@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode);
586  return;
587  }
588  _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
589  options:options
590  owner:self
591  userInfo:nil];
592  [self.flutterView addTrackingArea:_trackingArea];
593  } else if (_trackingArea) {
594  [self.flutterView removeTrackingArea:_trackingArea];
595  _trackingArea = nil;
596  }
597 }
598 
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];
604 }
605 
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];
615  } else {
616  // Waiting until the first momentum change event is a workaround for an issue where
617  // touchesBegan: is called unexpectedly while in low power mode within the interval between
618  // momentum start and the first momentum change.
619  if (event.momentumPhase == NSEventPhaseChanged) {
620  _mouseState.last_scroll_momentum_changed_time = event.timestamp;
621  }
622  // Skip momentum update events, the framework will generate scroll momentum.
623  NSAssert(event.momentumPhase != NSEventPhaseNone,
624  @"Received gesture event with unexpected phase");
625  }
626 }
627 
628 - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
629  NSAssert(self.viewLoaded, @"View must be loaded before it handles the mouse event");
630  // There are edge cases where the system will deliver enter out of order relative to other
631  // events (e.g., drag out and back in, release, then click; mouseDown: will be called before
632  // mouseEntered:). Discard those events, since the add will already have been synthesized.
633  if (_mouseState.flutter_state_is_added && phase == kAdd) {
634  return;
635  }
636 
637  // Multiple gesture recognizers could be active at once, we can't send multiple kPanZoomStart.
638  // For example: rotation and magnification.
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;
646  }
647  }
648  if (phase == kPanZoomStart) {
649  if (event.type == NSEventTypeScrollWheel) {
650  // Ensure scroll inertia cancel event is not sent afterwards.
651  _mouseState.last_scroll_momentum_changed_time = 0;
652  }
653  if (_mouseState.flutter_state_is_pan_zoom_started) {
654  // Already started on a previous gesture type
655  return;
656  }
657  _mouseState.flutter_state_is_pan_zoom_started = true;
658  }
659  if (phase == kPanZoomEnd) {
660  if (!_mouseState.flutter_state_is_pan_zoom_started) {
661  // NSEventPhaseCancelled is sometimes received at incorrect times in the state
662  // machine, just ignore it here if it doesn't make sense
663  // (we have no active gesture to cancel).
664  NSAssert(event.phase == NSEventPhaseCancelled,
665  @"Received gesture event with unexpected phase");
666  return;
667  }
668  // NSEventPhase values are powers of two, we can use this to inspect merged phases.
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) {
674  // Even though this gesture type ended, a different type is still active.
675  return;
676  }
677  }
678 
679  // If a pointer added event hasn't been sent, synthesize one using this event for the basic
680  // information.
681  if (!_mouseState.flutter_state_is_added && phase != kAdd) {
682  // Only the values extracted for use in flutterEvent below matter, the rest are dummy values.
683  NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
684  location:event.locationInWindow
685  modifierFlags:0
686  timestamp:event.timestamp
687  windowNumber:event.windowNumber
688  context:nil
689  eventNumber:0
690  trackingNumber:0
691  userData:NULL];
692  [self dispatchMouseEvent:addEvent phase:kAdd];
693  }
694 
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;
702  }
703  FlutterPointerEvent flutterEvent = {
704  .struct_size = sizeof(flutterEvent),
705  .phase = phase,
706  .timestamp = static_cast<size_t>(event.timestamp * USEC_PER_SEC),
707  .x = locationInBackingCoordinates.x,
708  .y = -locationInBackingCoordinates.y, // convertPointToBacking makes this negative.
709  .device = device,
710  .device_kind = deviceKind,
711  // If a click triggered a synthesized kAdd, don't pass the buttons in that event.
712  .buttons = phase == kAdd ? 0 : _mouseState.buttons,
713  .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
714  };
715 
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);
724  }
725  flutterEvent.pan_x = _mouseState.delta_x;
726  flutterEvent.pan_y = _mouseState.delta_y;
727  // Scale value needs to be normalized to range 0->infinity.
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;
734 
735  double pixelsPerLine = 1.0;
736  if (!event.hasPreciseScrollingDeltas) {
737  // The scrollingDelta needs to be multiplied by the line height.
738  // CGEventSourceGetPixelsPerLine() will return 10, which will result in
739  // scrolling that is noticeably slower than in other applications.
740  // Using 40.0 as the multiplier to match Chromium.
741  // See https://source.chromium.org/chromium/chromium/src/+/main:ui/events/cocoa/events_mac.mm
742  pixelsPerLine = 40.0;
743  }
744  double scaleFactor = self.flutterView.layer.contentsScale;
745  // When mouse input is received while shift is pressed (regardless of
746  // any other pressed keys), Mac automatically flips the axis. Other
747  // platforms do not do this, so we flip it back to normalize the input
748  // received by the framework. The keyboard+mouse-scroll mechanism is exposed
749  // in the ScrollBehavior of the framework so developers can customize the
750  // behavior.
751  // At time of change, Apple does not expose any other type of API or signal
752  // that the X/Y axes have been flipped.
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;
758  } else {
759  flutterEvent.scroll_delta_x = scaledDeltaX;
760  flutterEvent.scroll_delta_y = scaledDeltaY;
761  }
762  }
763 
764  [_engine.keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
765  [_engine sendPointerEvent:flutterEvent];
766 
767  // Update tracking of state as reported to Flutter.
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;
775  }
776  } else if (phase == kAdd) {
777  _mouseState.flutter_state_is_added = true;
778  } else if (phase == kRemove) {
779  _mouseState.Reset();
780  }
781 }
782 
783 - (void)onAccessibilityStatusChanged:(BOOL)enabled {
784  if (!enabled && self.viewLoaded && [self.activeTextInputPlugin isFirstResponder]) {
785  // Normally TextInputPlugin, when editing, is child of FlutterViewWrapper.
786  // When accessibility is enabled the TextInputPlugin gets added as an indirect
787  // child to FlutterTextField. When disabling the plugin needs to be reparented
788  // back.
789  [self.view addSubview:self.activeTextInputPlugin];
790  }
791 }
792 
793 - (std::shared_ptr<flutter::AccessibilityBridgeMac>)createAccessibilityBridgeWithEngine:
794  (nonnull FlutterEngine*)engine {
795  return std::make_shared<flutter::AccessibilityBridgeMac>(engine, self);
796 }
797 
798 - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id<MTLDevice>)device
799  commandQueue:(id<MTLCommandQueue>)commandQueue {
800  FlutterDartProject* project = _project ?: self.engine.project;
801  return [[FlutterView alloc] initWithMTLDevice:device
802  commandQueue:commandQueue
803  delegate:self
804  viewIdentifier:_viewIdentifier
805  enableWideGamut:project.enableWideGamut];
806 }
807 
808 - (void)updateWideGamutForScreen {
809  FlutterDartProject* project = _project ?: self.engine.project;
810  if (!project.enableWideGamut) {
811  return;
812  }
813  NSScreen* screen = self.view.window.screen;
814  if (screen == nil) {
815  return;
816  }
817  BOOL screenSupportsP3 = [screen canRepresentDisplayGamut:NSDisplayGamutP3];
818  [self.flutterView setEnableWideGamut:screenSupportsP3];
819 }
820 
821 - (NSString*)lookupKeyForAsset:(NSString*)asset {
822  return [FlutterDartProject lookupKeyForAsset:asset];
823 }
824 
825 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
826  return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
827 }
828 
829 #pragma mark - FlutterViewDelegate
830 
831 /**
832  * Responds to view reshape by notifying the engine of the change in dimensions.
833  */
834 - (void)viewDidReshape:(NSView*)view {
835  FML_DCHECK(view == _flutterView);
836  [_engine updateWindowMetricsForViewController:self];
837 }
838 
839 - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view {
840  FML_DCHECK(view == _flutterView);
841  // Only allow FlutterView to become first responder if TextInputPlugin is
842  // not active. Otherwise a mouse event inside FlutterView would cause the
843  // TextInputPlugin to lose first responder status.
844  return !self.activeTextInputPlugin.isFirstResponder;
845 }
846 
847 #pragma mark - FlutterPluginRegistry
848 
849 - (id<FlutterPluginRegistrar>)registrarForPlugin:(NSString*)pluginName {
850  return [_engine registrarForPlugin:pluginName];
851 }
852 
853 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
854  return [_engine valuePublishedByPlugin:pluginKey];
855 }
856 
857 #pragma mark - FlutterKeyboardViewDelegate
858 
859 - (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event {
860  return [self.activeTextInputPlugin handleKeyEvent:event];
861 }
862 
863 #pragma mark - NSResponder
864 
865 - (BOOL)acceptsFirstResponder {
866  return YES;
867 }
868 
869 - (void)keyDown:(NSEvent*)event {
870  [_engine.keyboardManager handleEvent:event withContext:self];
871 }
872 
873 - (void)keyUp:(NSEvent*)event {
874  [_engine.keyboardManager handleEvent:event withContext:self];
875 }
876 
877 - (void)flagsChanged:(NSEvent*)event {
878  [_engine.keyboardManager handleEvent:event withContext:self];
879 }
880 
881 - (void)mouseEntered:(NSEvent*)event {
882  if (_mouseState.has_pending_exit) {
883  _mouseState.has_pending_exit = false;
884  } else {
885  [self dispatchMouseEvent:event phase:kAdd];
886  }
887 }
888 
889 - (void)mouseExited:(NSEvent*)event {
890  if (_mouseState.buttons != 0) {
891  _mouseState.has_pending_exit = true;
892  return;
893  }
894  [self dispatchMouseEvent:event phase:kRemove];
895 }
896 
897 - (void)mouseDown:(NSEvent*)event {
898  _mouseState.buttons |= kFlutterPointerButtonMousePrimary;
899  [self dispatchMouseEvent:event];
900 }
901 
902 - (void)mouseUp:(NSEvent*)event {
903  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary);
904  [self dispatchMouseEvent:event];
905 }
906 
907 - (void)mouseDragged:(NSEvent*)event {
908  [self dispatchMouseEvent:event];
909 }
910 
911 - (void)rightMouseDown:(NSEvent*)event {
912  _mouseState.buttons |= kFlutterPointerButtonMouseSecondary;
913  [self dispatchMouseEvent:event];
914 }
915 
916 - (void)rightMouseUp:(NSEvent*)event {
917  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary);
918  [self dispatchMouseEvent:event];
919 }
920 
921 - (void)rightMouseDragged:(NSEvent*)event {
922  [self dispatchMouseEvent:event];
923 }
924 
925 - (void)otherMouseDown:(NSEvent*)event {
926  _mouseState.buttons |= (1 << event.buttonNumber);
927  [self dispatchMouseEvent:event];
928 }
929 
930 - (void)otherMouseUp:(NSEvent*)event {
931  _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber);
932  [self dispatchMouseEvent:event];
933 }
934 
935 - (void)otherMouseDragged:(NSEvent*)event {
936  [self dispatchMouseEvent:event];
937 }
938 
939 - (void)mouseMoved:(NSEvent*)event {
940  [self dispatchMouseEvent:event];
941 }
942 
943 - (void)scrollWheel:(NSEvent*)event {
944  [self dispatchGestureEvent:event];
945 }
946 
947 - (void)magnifyWithEvent:(NSEvent*)event {
948  [self dispatchGestureEvent:event];
949 }
950 
951 - (void)rotateWithEvent:(NSEvent*)event {
952  [self dispatchGestureEvent:event];
953 }
954 
955 - (void)swipeWithEvent:(NSEvent*)event {
956  // Not needed, it's handled by scrollWheel.
957 }
958 
959 - (void)touchesBeganWithEvent:(NSEvent*)event {
960  NSTouch* touch = event.allTouches.anyObject;
961  if (touch != nil) {
962  if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
963  kTrackpadTouchInertiaCancelWindowMs) {
964  // The trackpad has been touched following a scroll momentum event.
965  // A scroll inertia cancel message should be sent to the framework.
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, // convertPointToBacking makes this negative.
974  .device = kPointerPanZoomDeviceId,
975  .signal_kind = kFlutterPointerSignalKindScrollInertiaCancel,
976  .device_kind = kFlutterPointerDeviceKindTrackpad,
977  .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
978  };
979 
980  [_engine sendPointerEvent:flutterEvent];
981  // Ensure no further scroll inertia cancel event will be sent.
982  _mouseState.last_scroll_momentum_changed_time = 0;
983  }
984  }
985 }
986 
987 @end
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)