Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
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
25
26#pragma mark - Static types and data.
27
28namespace {
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).
32static constexpr int32_t kMousePointerDeviceId = 0;
33static 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.
38static 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 */
44struct 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;
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 */
321static 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++) {
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];
699 if (phase == kPanZoomStart || phase == kPanZoomUpdate || phase == kPanZoomEnd) {
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) {
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 {
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 {
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 {
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.
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
FlutterPointerPhase
The phase of the pointer event.
Definition embedder.h:1267
@ kPanZoomUpdate
The pan/zoom updated.
Definition embedder.h:1303
@ kHover
The pointer moved while up.
Definition embedder.h:1299
@ kUp
Definition embedder.h:1275
@ kPanZoomStart
A pan/zoom started on this pointer.
Definition embedder.h:1301
@ kRemove
Definition embedder.h:1297
@ kDown
Definition embedder.h:1282
@ kAdd
Definition embedder.h:1292
@ kMove
Definition embedder.h:1287
@ kPanZoomEnd
The pan/zoom ended.
Definition embedder.h:1305
@ kFlutterPointerButtonMousePrimary
Definition embedder.h:1319
@ kFlutterPointerButtonMouseSecondary
Definition embedder.h:1320
@ kFlutterPointerSignalKindScrollInertiaCancel
Definition embedder.h:1332
@ kFlutterPointerSignalKindScroll
Definition embedder.h:1331
FlutterPointerDeviceKind
The device type that created a pointer event.
Definition embedder.h:1309
@ kFlutterPointerDeviceKindTrackpad
Definition embedder.h:1313
@ kFlutterPointerDeviceKindMouse
Definition embedder.h:1310
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:1355
size_t struct_size
The size of this struct. Must be sizeof(FlutterPointerEvent).
Definition embedder.h:1338
double scale
The scale of the pan/zoom, where 1.0 is the initial scale.
Definition embedder.h:1369
FlutterPointerSignalKind signal_kind
Definition embedder.h:1351
double rotation
The rotation of the pan/zoom in radians, where 0.0 is the initial angle.
Definition embedder.h:1371
double scroll_delta_x
The x offset of the scroll in physical pixels.
Definition embedder.h:1353
double pan_x
The x offset of the pan/zoom in physical pixels.
Definition embedder.h:1365
double pan_y
The y offset of the pan/zoom in physical pixels.
Definition embedder.h:1367
A batch of updates to semantics nodes and custom actions.
Definition embedder.h:1826
size_t node_count
The number of semantics node updates.
Definition embedder.h:1830
size_t custom_action_count
The number of semantics custom action updates.
Definition embedder.h:1834
FlutterSemanticsNode2 ** nodes
Definition embedder.h:1832
FlutterSemanticsCustomAction2 ** custom_actions
Definition embedder.h:1837
const uintptr_t id
int BOOL