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