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