| 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
5 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
6 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
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"
13 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
14 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
15 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
16 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
17 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
18 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h"
19 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h"
20 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
21 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
22 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h"
23 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView_Internal.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 */
163 @interface FlutterViewController () <FlutterViewDelegate, FlutterKeyboardManagerEventContext>
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;
214 __weak FlutterViewController* _controller;
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.
297 FlutterDartProject* _project;
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
FlutterChannels.h
FlutterCodecs.h
FlutterDartProject_Internal.h
FlutterEngine.h
_project
FlutterDartProject * _project
Definition: FlutterEngine.mm:415
FlutterEngine_Internal.h
FlutterKeyPrimaryResponder.h
FlutterKeyboardManager.h
FlutterRenderer.h
FlutterTextInputSemanticsObject.h
FlutterView.h
FlutterView_Internal.h
FlutterViewController.h
FlutterViewIdentifier
int64_t FlutterViewIdentifier
Definition: FlutterViewController.h:21
_controller
__weak FlutterViewController * _controller
Definition: FlutterViewController.mm:212
_bridge
std::shared_ptr< flutter::AccessibilityBridgeMac > _bridge
Definition: FlutterViewController.mm:295
FlutterViewController_Internal.h
FlutterDartProject
Definition: FlutterDartProject.mm:39
+[FlutterDartProject lookupKeyForAsset:fromPackage:]
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
Definition: FlutterDartProject.mm:166
+[FlutterDartProject lookupKeyForAsset:]
NSString * lookupKeyForAsset:(NSString *asset)
Definition: FlutterDartProject.mm:157
FlutterEngine
Definition: FlutterEngine.h:32
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:42
FlutterViewController
Definition: FlutterViewController.h:73
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
FlutterViewController::viewIdentifier
FlutterViewIdentifier viewIdentifier
Definition: FlutterViewController.h:130
FlutterView
Definition: FlutterView.h:69
-[FlutterView setBackgroundColor:]
void setBackgroundColor:(nonnull NSColor *color)
FlutterViewWrapper
Definition: FlutterViewController.mm:155
FlutterKeyboardManagerEventContext-p
Definition: FlutterKeyboardManager.h:40
FlutterPluginRegistrar-p
Definition: FlutterPluginRegistrarMacOS.h:28
FlutterViewDelegate-p
Definition: FlutterView.h:51