Adds macOS raw keyboard mapping (#29231)


diff --git a/dev/manual_tests/lib/raw_keyboard.dart b/dev/manual_tests/lib/raw_keyboard.dart
index 06ba889..a2474ee 100644
--- a/dev/manual_tests/lib/raw_keyboard.dart
+++ b/dev/manual_tests/lib/raw_keyboard.dart
@@ -92,6 +92,11 @@
             dataText.add(Text('codePoint: ${data.codePoint} (${_asHex(data.codePoint)})'));
             dataText.add(Text('hidUsage: ${data.hidUsage} (${_asHex(data.hidUsage)})'));
             dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
+          } else if (data is RawKeyEventDataMacOs) {
+            dataText.add(Text('keyCode: ${data.keyCode} (${_asHex(data.keyCode)})'));
+            dataText.add(Text('characters: ${data.characters}'));
+            dataText.add(Text('charactersIgnoringModifiers: ${data.charactersIgnoringModifiers}'));
+            dataText.add(Text('modifiers: ${data.modifiers} (${_asHex(data.modifiers)})'));
           }
           dataText.add(Text('logical: ${_event.logicalKey}'));
           dataText.add(Text('physical: ${_event.physicalKey}'));
diff --git a/dev/tools/gen_keycodes/data/keyboard_maps.tmpl b/dev/tools/gen_keycodes/data/keyboard_maps.tmpl
index cdd18a7..06e6352 100644
--- a/dev/tools/gen_keycodes/data/keyboard_maps.tmpl
+++ b/dev/tools/gen_keycodes/data/keyboard_maps.tmpl
@@ -38,3 +38,15 @@
 const Map<int, PhysicalKeyboardKey> kFuchsiaToPhysicalKey = <int, PhysicalKeyboardKey>{
 @@@FUCHSIA_SCAN_CODE_MAP@@@
 };
+
+/// Maps macOS-specific key code values representing [PhysicalKeyboardKey].
+const Map<int, PhysicalKeyboardKey> kMacOsToPhysicalKey = <int, PhysicalKeyboardKey>{
+@@@MACOS_SCAN_CODE_MAP@@@
+};
+
+/// A map of macOS key codes which have printable representations, but appear
+/// on the number pad. Used to provide different key objects for keys like
+/// KEY_EQUALS and NUMPAD_EQUALS.
+const Map<int, LogicalKeyboardKey> kMacOsNumPadMap = <int, LogicalKeyboardKey>{
+@@@MACOS_NUMPAD_MAP@@@
+};
diff --git a/dev/tools/gen_keycodes/lib/code_gen.dart b/dev/tools/gen_keycodes/lib/code_gen.dart
index 3a8be0f..0ba5308 100644
--- a/dev/tools/gen_keycodes/lib/code_gen.dart
+++ b/dev/tools/gen_keycodes/lib/code_gen.dart
@@ -135,6 +135,31 @@
     return androidScanCodeMap.toString().trimRight();
   }
 
+  /// This generates the map of macOS key codes to physical keys.
+  String get macOsScanCodeMap {
+    final StringBuffer macOsScanCodeMap = StringBuffer();
+    for (Key entry in keyData.data) {
+      if (entry.macOsScanCode != null) {
+        macOsScanCodeMap.writeln('  ${toHex(entry.macOsScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
+      }
+    }
+    return macOsScanCodeMap.toString().trimRight();
+  }
+
+  /// This generates the map of macOS number pad key codes to logical keys.
+  String get macOsNumpadMap {
+    final StringBuffer macOsNumPadMap = StringBuffer();
+    final List<Key> onlyNumpads = keyData.data.where((Key entry) {
+      return entry.constantName.startsWith('numpad') && entry.keyLabel != null;
+    }).toList();
+    for (Key entry in onlyNumpads) {
+      if (entry.macOsScanCode != null) {
+        macOsNumPadMap.writeln('  ${toHex(entry.macOsScanCode)}: LogicalKeyboardKey.${entry.constantName},');
+      }
+    }
+    return macOsNumPadMap.toString().trimRight();
+  }
+
   /// This generates the map of Fuchsia key codes to logical keys.
   String get fuchsiaKeyCodeMap {
     final StringBuffer fuchsiaKeyCodeMap = StringBuffer();
@@ -174,12 +199,17 @@
   /// Substitutes the various platform specific maps into the template file for
   /// keyboard_maps.dart.
   String generateKeyboardMaps() {
+    // There is no macOS keycode map since macOS uses keycode to represent a physical key.
+    // The LogicalKeyboardKey is generated by raw_keyboard_macos.dart from the unmodified characters
+    // from NSEvent.
     final Map<String, String> mappings = <String, String>{
       'ANDROID_SCAN_CODE_MAP': androidScanCodeMap,
       'ANDROID_KEY_CODE_MAP': androidKeyCodeMap,
       'ANDROID_NUMPAD_MAP': androidNumpadMap,
       'FUCHSIA_SCAN_CODE_MAP': fuchsiaHidCodeMap,
       'FUCHSIA_KEY_CODE_MAP': fuchsiaKeyCodeMap,
+      'MACOS_SCAN_CODE_MAP': macOsScanCodeMap,
+      'MACOS_NUMPAD_MAP': macOsNumpadMap,
     };
 
     final String template = File(path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data', 'keyboard_maps.tmpl')).readAsStringSync();
diff --git a/packages/flutter/lib/services.dart b/packages/flutter/lib/services.dart
index ff56d86..3a916fb 100644
--- a/packages/flutter/lib/services.dart
+++ b/packages/flutter/lib/services.dart
@@ -24,6 +24,7 @@
 export 'src/services/raw_keyboard.dart';
 export 'src/services/raw_keyboard_android.dart';
 export 'src/services/raw_keyboard_fuchsia.dart';
+export 'src/services/raw_keyboard_macos.dart';
 export 'src/services/system_channels.dart';
 export 'src/services/system_chrome.dart';
 export 'src/services/system_navigator.dart';
diff --git a/packages/flutter/lib/src/services/keyboard_maps.dart b/packages/flutter/lib/src/services/keyboard_maps.dart
index a718a3a..0e03e08 100644
--- a/packages/flutter/lib/src/services/keyboard_maps.dart
+++ b/packages/flutter/lib/src/services/keyboard_maps.dart
@@ -842,3 +842,150 @@
   0x000c028b: PhysicalKeyboardKey.mailForward,
   0x000c028c: PhysicalKeyboardKey.mailSend,
 };
+
+
+/// Maps macOS-specific key code values representing [PhysicalKeyboardKey].
+/// MacOS doesn't provide a scan code, but a virtual keycode to represent a physical key.
+const Map<int, PhysicalKeyboardKey> kMacOsToPhysicalKey = <int, PhysicalKeyboardKey>{
+  0x00000000: PhysicalKeyboardKey.keyA,
+  0x0000000b: PhysicalKeyboardKey.keyB,
+  0x00000008: PhysicalKeyboardKey.keyC,
+  0x00000002: PhysicalKeyboardKey.keyD,
+  0x0000000e: PhysicalKeyboardKey.keyE,
+  0x00000003: PhysicalKeyboardKey.keyF,
+  0x00000005: PhysicalKeyboardKey.keyG,
+  0x00000004: PhysicalKeyboardKey.keyH,
+  0x00000022: PhysicalKeyboardKey.keyI,
+  0x00000026: PhysicalKeyboardKey.keyJ,
+  0x00000028: PhysicalKeyboardKey.keyK,
+  0x00000025: PhysicalKeyboardKey.keyL,
+  0x0000002e: PhysicalKeyboardKey.keyM,
+  0x0000002d: PhysicalKeyboardKey.keyN,
+  0x0000001f: PhysicalKeyboardKey.keyO,
+  0x00000023: PhysicalKeyboardKey.keyP,
+  0x0000000c: PhysicalKeyboardKey.keyQ,
+  0x0000000f: PhysicalKeyboardKey.keyR,
+  0x00000001: PhysicalKeyboardKey.keyS,
+  0x00000011: PhysicalKeyboardKey.keyT,
+  0x00000020: PhysicalKeyboardKey.keyU,
+  0x00000009: PhysicalKeyboardKey.keyV,
+  0x0000000d: PhysicalKeyboardKey.keyW,
+  0x00000007: PhysicalKeyboardKey.keyX,
+  0x00000010: PhysicalKeyboardKey.keyY,
+  0x00000006: PhysicalKeyboardKey.keyZ,
+  0x00000012: PhysicalKeyboardKey.digit1,
+  0x00000013: PhysicalKeyboardKey.digit2,
+  0x00000014: PhysicalKeyboardKey.digit3,
+  0x00000015: PhysicalKeyboardKey.digit4,
+  0x00000017: PhysicalKeyboardKey.digit5,
+  0x00000016: PhysicalKeyboardKey.digit6,
+  0x0000001a: PhysicalKeyboardKey.digit7,
+  0x0000001c: PhysicalKeyboardKey.digit8,
+  0x00000019: PhysicalKeyboardKey.digit9,
+  0x0000001d: PhysicalKeyboardKey.digit0,
+  0x00000024: PhysicalKeyboardKey.enter,
+  0x00000035: PhysicalKeyboardKey.escape,
+  0x00000033: PhysicalKeyboardKey.backspace,
+  0x00000030: PhysicalKeyboardKey.tab,
+  0x00000031: PhysicalKeyboardKey.space,
+  0x0000001b: PhysicalKeyboardKey.minus,
+  0x00000018: PhysicalKeyboardKey.equal,
+  0x00000021: PhysicalKeyboardKey.bracketLeft,
+  0x0000001e: PhysicalKeyboardKey.bracketRight,
+  0x0000002a: PhysicalKeyboardKey.backslash,
+  0x00000029: PhysicalKeyboardKey.semicolon,
+  0x00000027: PhysicalKeyboardKey.quote,
+  0x00000032: PhysicalKeyboardKey.backquote,
+  0x0000002b: PhysicalKeyboardKey.comma,
+  0x0000002f: PhysicalKeyboardKey.period,
+  0x0000002c: PhysicalKeyboardKey.slash,
+  0x00000039: PhysicalKeyboardKey.capsLock,
+  0x0000007a: PhysicalKeyboardKey.f1,
+  0x00000078: PhysicalKeyboardKey.f2,
+  0x00000063: PhysicalKeyboardKey.f3,
+  0x00000076: PhysicalKeyboardKey.f4,
+  0x00000060: PhysicalKeyboardKey.f5,
+  0x00000061: PhysicalKeyboardKey.f6,
+  0x00000062: PhysicalKeyboardKey.f7,
+  0x00000064: PhysicalKeyboardKey.f8,
+  0x00000065: PhysicalKeyboardKey.f9,
+  0x0000006d: PhysicalKeyboardKey.f10,
+  0x00000067: PhysicalKeyboardKey.f11,
+  0x0000006f: PhysicalKeyboardKey.f12,
+  0x00000072: PhysicalKeyboardKey.insert,
+  0x00000073: PhysicalKeyboardKey.home,
+  0x00000074: PhysicalKeyboardKey.pageUp,
+  0x00000075: PhysicalKeyboardKey.delete,
+  0x00000077: PhysicalKeyboardKey.end,
+  0x00000079: PhysicalKeyboardKey.pageDown,
+  0x0000007c: PhysicalKeyboardKey.arrowRight,
+  0x0000007b: PhysicalKeyboardKey.arrowLeft,
+  0x0000007d: PhysicalKeyboardKey.arrowDown,
+  0x0000007e: PhysicalKeyboardKey.arrowUp,
+  0x00000047: PhysicalKeyboardKey.numLock,
+  0x0000004b: PhysicalKeyboardKey.numpadDivide,
+  0x00000043: PhysicalKeyboardKey.numpadMultiply,
+  0x0000004e: PhysicalKeyboardKey.numpadSubtract,
+  0x00000045: PhysicalKeyboardKey.numpadAdd,
+  0x0000004c: PhysicalKeyboardKey.numpadEnter,
+  0x00000053: PhysicalKeyboardKey.numpad1,
+  0x00000054: PhysicalKeyboardKey.numpad2,
+  0x00000055: PhysicalKeyboardKey.numpad3,
+  0x00000056: PhysicalKeyboardKey.numpad4,
+  0x00000057: PhysicalKeyboardKey.numpad5,
+  0x00000058: PhysicalKeyboardKey.numpad6,
+  0x00000059: PhysicalKeyboardKey.numpad7,
+  0x0000005b: PhysicalKeyboardKey.numpad8,
+  0x0000005c: PhysicalKeyboardKey.numpad9,
+  0x00000052: PhysicalKeyboardKey.numpad0,
+  0x00000041: PhysicalKeyboardKey.numpadDecimal,
+  0x0000000a: PhysicalKeyboardKey.intlBackslash,
+  0x0000006e: PhysicalKeyboardKey.contextMenu,
+  0x00000051: PhysicalKeyboardKey.numpadEqual,
+  0x00000069: PhysicalKeyboardKey.f13,
+  0x0000006b: PhysicalKeyboardKey.f14,
+  0x00000071: PhysicalKeyboardKey.f15,
+  0x0000006a: PhysicalKeyboardKey.f16,
+  0x00000040: PhysicalKeyboardKey.f17,
+  0x0000004f: PhysicalKeyboardKey.f18,
+  0x00000050: PhysicalKeyboardKey.f19,
+  0x0000005a: PhysicalKeyboardKey.f20,
+  0x0000004a: PhysicalKeyboardKey.audioVolumeMute,
+  0x00000048: PhysicalKeyboardKey.audioVolumeUp,
+  0x00000049: PhysicalKeyboardKey.audioVolumeDown,
+  0x0000005f: PhysicalKeyboardKey.numpadComma,
+  0x0000005e: PhysicalKeyboardKey.intlRo,
+  0x00000068: PhysicalKeyboardKey.kanaMode,
+  0x0000005d: PhysicalKeyboardKey.intlYen,
+  0x0000003b: PhysicalKeyboardKey.controlLeft,
+  0x00000038: PhysicalKeyboardKey.shiftLeft,
+  0x0000003a: PhysicalKeyboardKey.altLeft,
+  0x00000037: PhysicalKeyboardKey.metaLeft,
+  0x0000003e: PhysicalKeyboardKey.controlRight,
+  0x0000003c: PhysicalKeyboardKey.shiftRight,
+  0x0000003d: PhysicalKeyboardKey.altRight,
+  0x00000036: PhysicalKeyboardKey.metaRight,
+};
+
+/// A map of macOS key codes which have printable representations, but appear
+/// on the number pad. Used to provide different key objects for keys like
+/// KEY_EQUALS and NUMPAD_EQUALS.
+const Map<int, LogicalKeyboardKey> kMacOsNumPadMap = <int, LogicalKeyboardKey>{
+  0x0000004b: LogicalKeyboardKey.numpadDivide,
+  0x00000043: LogicalKeyboardKey.numpadMultiply,
+  0x0000004e: LogicalKeyboardKey.numpadSubtract,
+  0x00000045: LogicalKeyboardKey.numpadAdd,
+  0x00000053: LogicalKeyboardKey.numpad1,
+  0x00000054: LogicalKeyboardKey.numpad2,
+  0x00000055: LogicalKeyboardKey.numpad3,
+  0x00000056: LogicalKeyboardKey.numpad4,
+  0x00000057: LogicalKeyboardKey.numpad5,
+  0x00000058: LogicalKeyboardKey.numpad6,
+  0x00000059: LogicalKeyboardKey.numpad7,
+  0x0000005b: LogicalKeyboardKey.numpad8,
+  0x0000005c: LogicalKeyboardKey.numpad9,
+  0x00000052: LogicalKeyboardKey.numpad0,
+  0x00000041: LogicalKeyboardKey.numpadDecimal,
+  0x00000051: LogicalKeyboardKey.numpadEqual,
+  0x0000005f: LogicalKeyboardKey.numpadComma,
+};
diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart
index 02c1441..6c46d12 100644
--- a/packages/flutter/lib/src/services/raw_keyboard.dart
+++ b/packages/flutter/lib/src/services/raw_keyboard.dart
@@ -9,6 +9,7 @@
 import 'keyboard_key.dart';
 import 'raw_keyboard_android.dart';
 import 'raw_keyboard_fuchsia.dart';
+import 'raw_keyboard_macos.dart';
 import 'system_channels.dart';
 
 /// An enum describing the side of the keyboard that a key is on, to allow
@@ -263,6 +264,14 @@
           modifiers: message['modifiers'] ?? 0,
         );
         break;
+      case 'macos':
+        data = RawKeyEventDataMacOs(
+            characters: message['characters'] ?? '',
+            charactersIgnoringModifiers:
+                message['charactersIgnoringModifiers'] ?? '',
+            keyCode: message['keyCode'] ?? 0,
+            modifiers: message['modifiers'] ?? 0);
+        break;
       default:
         // We don't yet implement raw key events on iOS or other platforms, but
         // we don't hit this exception because the engine never sends us these
@@ -400,7 +409,7 @@
   const RawKeyDownEvent({
     @required RawKeyEventData data,
     String character,
-  })  : super(data: data, character: character);
+  }) : super(data: data, character: character);
 }
 
 /// The user has released a key on the keyboard.
diff --git a/packages/flutter/lib/src/services/raw_keyboard_macos.dart b/packages/flutter/lib/src/services/raw_keyboard_macos.dart
new file mode 100644
index 0000000..2c48059
--- /dev/null
+++ b/packages/flutter/lib/src/services/raw_keyboard_macos.dart
@@ -0,0 +1,315 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+
+import 'keyboard_key.dart';
+import 'keyboard_maps.dart';
+import 'raw_keyboard.dart';
+
+/// Platform-specific key event data for macOS.
+///
+/// This object contains information about key events obtained from macOS's
+/// `NSEvent` interface.
+///
+/// See also:
+///
+///  * [RawKeyboard], which uses this interface to expose key data.
+class RawKeyEventDataMacOs extends RawKeyEventData {
+  /// Creates a key event data structure specific for macOS.
+  ///
+  /// The [characters], [charactersIgnoringModifiers], and [modifiers], arguments
+  /// must not be null.
+  const RawKeyEventDataMacOs({
+    this.characters = '',
+    this.charactersIgnoringModifiers = '',
+    this.keyCode = 0,
+    this.modifiers = 0,
+  }) : assert(characters != null),
+       assert(charactersIgnoringModifiers != null),
+       assert(keyCode != null),
+       assert(modifiers != null);
+
+  /// The Unicode characters associated with a key-up or key-down event.
+  ///
+  /// See also:
+  ///
+  ///   * [Apple's NSEvent documentation](https://developer.apple.com/documentation/appkit/nsevent/1534183-characters?language=objc)
+  final String characters;
+
+  /// The characters generated by a key event as if no modifier key (except for
+  /// Shift) applies.
+  ///
+  /// See also:
+  ///
+  ///   * [Apple's NSEvent documentation](https://developer.apple.com/documentation/appkit/nsevent/1524605-charactersignoringmodifiers?language=objc)
+  final String charactersIgnoringModifiers;
+
+  /// The virtual key code for the keyboard key associated with a key event.
+  ///
+  /// See also:
+  ///
+  ///   * [Apple's NSEvent documentation](https://developer.apple.com/documentation/appkit/nsevent/1534513-keycode?language=objc)
+  final int keyCode;
+
+  /// A mask of the current modifiers using the values in Modifier Flags.
+  ///
+  /// See also:
+  ///
+  ///   * [Apple's NSEvent documentation](https://developer.apple.com/documentation/appkit/nsevent/1535211-modifierflags?language=objc)
+  final int modifiers;
+
+  @override
+  String get keyLabel => charactersIgnoringModifiers.isEmpty ? null : charactersIgnoringModifiers;
+
+  @override
+  PhysicalKeyboardKey get physicalKey => kMacOsToPhysicalKey[keyCode] ?? PhysicalKeyboardKey.none;
+
+  @override
+  LogicalKeyboardKey get logicalKey {
+    // Look to see if the keyCode is a printable number pad key, so that a
+    // difference between regular keys (e.g. "=") and the number pad version
+    // (e.g. the "=" on the number pad) can be determined.
+    final LogicalKeyboardKey numPadKey = kMacOsNumPadMap[keyCode];
+    if (numPadKey != null) {
+      return numPadKey;
+    }
+
+    // Look to see if the keyCode is one we know about and have a mapping for.
+    if (keyLabel != null &&
+        !LogicalKeyboardKey.isControlCharacter(keyLabel)) {
+      assert(charactersIgnoringModifiers.length <= 2);
+      int codeUnit = charactersIgnoringModifiers.codeUnitAt(0);
+      if (charactersIgnoringModifiers.length == 2) {
+        // Not covering length > 2 case since > 1 is already unlikely.
+        final int secondCode = charactersIgnoringModifiers.codeUnitAt(1);
+        codeUnit = (codeUnit << 16) | secondCode;
+      }
+
+      final int keyId = LogicalKeyboardKey.unicodePlane | (codeUnit & LogicalKeyboardKey.valueMask);
+      return LogicalKeyboardKey.findKeyByKeyId(keyId) ?? LogicalKeyboardKey(
+        keyId,
+        keyLabel: keyLabel,
+        debugName: kReleaseMode ? null : 'Key ${keyLabel.toUpperCase()}',
+      );
+    }
+
+    // This is a non-printable key that we don't know about, so we mint a new
+    // code with the autogenerated bit set.
+    const int macOsKeyIdPlane = 0x00500000000;
+
+    // Keys like "backspace" won't have a character, but it's known by the physical keyboard.
+    // Since there is no logical keycode map for macOS (macOS uses the keycode to reference
+    // physical keys), a LogicalKeyboardKey is created with the physical key's HID usage and
+    // debugName. This avoids the need for duplicating the physical key map.
+    if (physicalKey != PhysicalKeyboardKey.none) {
+      final int keyId = physicalKey.usbHidUsage | LogicalKeyboardKey.hidPlane;
+      return LogicalKeyboardKey.findKeyByKeyId(keyId) ?? LogicalKeyboardKey(
+        keyId,
+        keyLabel: physicalKey.debugName,
+        debugName: physicalKey.debugName,
+      );
+    }
+
+    return LogicalKeyboardKey(
+      macOsKeyIdPlane | keyCode | LogicalKeyboardKey.autogeneratedMask,
+      debugName: kReleaseMode ? null : 'Unknown macOS key code $keyCode',
+    );
+  }
+
+  bool _isLeftRightModifierPressed(KeyboardSide side, int anyMask, int leftMask, int rightMask) {
+    if (modifiers & anyMask == 0) {
+      return false;
+    }
+    switch (side) {
+      case KeyboardSide.any:
+        return true;
+      case KeyboardSide.all:
+        return modifiers & leftMask != 0 && modifiers & rightMask != 0;
+      case KeyboardSide.left:
+        return modifiers & leftMask != 0;
+      case KeyboardSide.right:
+        return modifiers & rightMask != 0;
+    }
+    return false;
+  }
+
+  @override
+  bool isModifierPressed(ModifierKey key, {KeyboardSide side = KeyboardSide.any}) {
+    final int independentModifier = modifiers & deviceIndependentMask;
+    switch (key) {
+      case ModifierKey.controlModifier:
+        return _isLeftRightModifierPressed(side, independentModifier & modifierControl, modifierLeftControl, modifierRightControl);
+      case ModifierKey.shiftModifier:
+        return _isLeftRightModifierPressed(side, independentModifier & modifierShift, modifierLeftShift, modifierRightShift);
+      case ModifierKey.altModifier:
+        return _isLeftRightModifierPressed(side, independentModifier & modifierOption, modifierLeftOption, modifierRightOption);
+      case ModifierKey.metaModifier:
+        return _isLeftRightModifierPressed(side, independentModifier & modifierCommand, modifierLeftCommand, modifierRightCommand);
+      case ModifierKey.capsLockModifier:
+        return independentModifier & modifierCapsLock != 0;
+      case ModifierKey.numLockModifier:
+        return independentModifier & modifierNumericPad != 0;
+      case ModifierKey.functionModifier:
+        return independentModifier & modifierFunction != 0;
+      case ModifierKey.symbolModifier:
+      case ModifierKey.scrollLockModifier:
+        // These are not used in macOS keyboards.
+        return false;
+    }
+    return false;
+  }
+
+  @override
+  KeyboardSide getModifierSide(ModifierKey key) {
+    KeyboardSide findSide(int leftMask, int rightMask) {
+      final int combinedMask = leftMask | rightMask;
+      final int combined = modifiers & combinedMask;
+      if (combined == leftMask) {
+        return KeyboardSide.left;
+      } else if (combined == rightMask) {
+        return KeyboardSide.right;
+      } else if (combined == combinedMask) {
+        return KeyboardSide.all;
+      }
+      return null;
+    }
+
+    switch (key) {
+      case ModifierKey.controlModifier:
+        return findSide(modifierLeftControl, modifierRightControl);
+      case ModifierKey.shiftModifier:
+        return findSide(modifierLeftShift, modifierRightShift);
+      case ModifierKey.altModifier:
+        return findSide(modifierLeftOption, modifierRightOption);
+      case ModifierKey.metaModifier:
+        return findSide(modifierLeftCommand, modifierRightCommand);
+      case ModifierKey.capsLockModifier:
+      case ModifierKey.numLockModifier:
+      case ModifierKey.scrollLockModifier:
+      case ModifierKey.functionModifier:
+      case ModifierKey.symbolModifier:
+        return KeyboardSide.all;
+    }
+
+    assert(false, 'Not handling $key type properly.');
+    return null;
+  }
+
+  // Modifier key masks. See Apple's NSEvent documentation
+  // https://developer.apple.com/documentation/appkit/nseventmodifierflags?language=objc
+  // https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h.auto.html
+
+  /// This mask is used to check the [modifiers] field to test whether the CAPS
+  /// LOCK modifier key is on.
+  ///
+  /// {@template flutter.services.logicalKeyboardKey.modifiers}
+  /// Use this value if you need to decode the [modifiers] field yourself, but
+  /// it's much easier to use [isModifierPressed] if you just want to know if
+  /// a modifier is pressed.
+  /// {@endtemplate}
+  static const int modifierCapsLock = 0x10000;
+
+  /// This mask is used to check the [modifiers] field to test whether one of the
+  /// SHIFT modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierShift = 0x20000;
+
+  /// This mask is used to check the [modifiers] field to test whether the left
+  /// SHIFT modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierLeftShift = 0x02;
+
+  /// This mask is used to check the [modifiers] field to test whether the right
+  /// SHIFT modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierRightShift = 0x04;
+
+  /// This mask is used to check the [modifiers] field to test whether one of the
+  /// CTRL modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierControl = 0x40000;
+
+  /// This mask is used to check the [modifiers] field to test whether the left
+  /// CTRL modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierLeftControl = 0x01;
+
+  /// This mask is used to check the [modifiers] field to test whether the right
+  /// CTRL modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierRightControl = 0x2000;
+
+  /// This mask is used to check the [modifiers] field to test whether one of the
+  /// ALT modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierOption = 0x80000;
+
+  /// This mask is used to check the [modifiers] field to test whether the left
+  /// ALT modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierLeftOption = 0x20;
+
+  /// This mask is used to check the [modifiers] field to test whether the right
+  /// ALT modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierRightOption = 0x40;
+
+  /// This mask is used to check the [modifiers] field to test whether one of the
+  /// CMD modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierCommand = 0x100000;
+
+  /// This mask is used to check the [modifiers] field to test whether the left
+  /// CMD modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierLeftCommand = 0x08;
+
+  /// This mask is used to check the [modifiers] field to test whether the right
+  /// CMD modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierRightCommand = 0x10;
+
+  /// This mask is used to check the [modifiers] field to test whether any key in
+  /// the numeric keypad is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierNumericPad = 0x200000;
+
+  /// This mask is used to check the [modifiers] field to test whether the
+  /// HELP modifier key is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierHelp = 0x400000;
+
+  /// This mask is used to check the [modifiers] field to test whether one of the
+  /// FUNCTION modifier keys is pressed.
+  ///
+  /// {@macro flutter.services.logicalKeyboardKey.modifiers}
+  static const int modifierFunction = 0x800000;
+
+  /// Used to retrieve only the device-independent modifier flags, allowing
+  /// applications to mask off the device-dependent modifier flags, including
+  /// event coalescing information.
+  static const int deviceIndependentMask = 0xffff0000;
+
+  @override
+  String toString() {
+    return '$runtimeType(keyLabel: $keyLabel, keyCode: $keyCode, characters: $characters,'
+        ' unmodifiedCharacters: $charactersIgnoringModifiers, modifiers: $modifiers, '
+        'modifiers down: $modifiersPressed)';
+  }
+}
diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart
index 73ce54d..b5bd89d 100644
--- a/packages/flutter/test/services/raw_keyboard_test.dart
+++ b/packages/flutter/test/services/raw_keyboard_test.dart
@@ -264,4 +264,133 @@
       expect(data.keyLabel, isNull);
     });
   });
+    group('RawKeyEventDataMacOs', () {
+    const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
+      RawKeyEventDataMacOs.modifierOption | RawKeyEventDataMacOs.modifierLeftOption: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.left),
+      RawKeyEventDataMacOs.modifierOption | RawKeyEventDataMacOs.modifierRightOption: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.right),
+      RawKeyEventDataMacOs.modifierShift | RawKeyEventDataMacOs.modifierLeftShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.left),
+      RawKeyEventDataMacOs.modifierShift | RawKeyEventDataMacOs.modifierRightShift: _ModifierCheck(ModifierKey.shiftModifier, KeyboardSide.right),
+      RawKeyEventDataMacOs.modifierFunction: _ModifierCheck(ModifierKey.functionModifier, KeyboardSide.all),
+      RawKeyEventDataMacOs.modifierControl | RawKeyEventDataMacOs.modifierLeftControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.left),
+      RawKeyEventDataMacOs.modifierControl | RawKeyEventDataMacOs.modifierRightControl: _ModifierCheck(ModifierKey.controlModifier, KeyboardSide.right),
+      RawKeyEventDataMacOs.modifierCommand | RawKeyEventDataMacOs.modifierLeftCommand: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.left),
+      RawKeyEventDataMacOs.modifierCommand | RawKeyEventDataMacOs.modifierRightCommand: _ModifierCheck(ModifierKey.metaModifier, KeyboardSide.right),
+      RawKeyEventDataMacOs.modifierCapsLock: _ModifierCheck(ModifierKey.capsLockModifier, KeyboardSide.all),
+    };
+
+    test('modifier keys are recognized individually', () {
+      for (int modifier in modifierTests.keys) {
+        final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
+          'type': 'keydown',
+          'keymap': 'macos',
+          'keyCode': 0x04,
+          'characters': 'a',
+          'charactersIgnoringModifiers': 'a',
+          'modifiers': modifier,
+        });
+        final RawKeyEventDataMacOs data = event.data;
+        for (ModifierKey key in ModifierKey.values) {
+          if (modifierTests[modifier].key == key) {
+            expect(
+              data.isModifierPressed(key, side: modifierTests[modifier].side),
+              isTrue,
+              reason: "$key should be pressed with metaState $modifier, but isn't.",
+            );
+            expect(data.getModifierSide(key), equals(modifierTests[modifier].side));
+          } else {
+            expect(
+              data.isModifierPressed(key, side: modifierTests[modifier].side),
+              isFalse,
+              reason: '$key should not be pressed with metaState $modifier.',
+            );
+          }
+        }
+      }
+    });
+    test('modifier keys are recognized when combined', () {
+      for (int modifier in modifierTests.keys) {
+        if (modifier == RawKeyEventDataMacOs.modifierFunction) {
+          // No need to combine function key with itself.
+          continue;
+        }
+        final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
+          'type': 'keydown',
+          'keymap': 'macos',
+          'keyCode': 0x04,
+          'plainCodePoint': 0x64,
+          'characters': 'a',
+          'charactersIgnoringModifiers': 'a',
+          'modifiers': modifier | RawKeyEventDataMacOs.modifierFunction,
+        });
+        final RawKeyEventDataMacOs data = event.data;
+        for (ModifierKey key in ModifierKey.values) {
+          if (modifierTests[modifier].key == key || key == ModifierKey.functionModifier) {
+            expect(
+              data.isModifierPressed(key, side: modifierTests[modifier].side),
+              isTrue,
+              reason: '$key should be pressed with metaState $modifier '
+                  "and additional key ${RawKeyEventDataMacOs.modifierFunction}, but isn't.",
+            );
+            if (key != ModifierKey.functionModifier) {
+              expect(data.getModifierSide(key), equals(modifierTests[modifier].side));
+            } else {
+              expect(data.getModifierSide(key), equals(KeyboardSide.all));
+            }
+          } else {
+            expect(
+              data.isModifierPressed(key, side: modifierTests[modifier].side),
+              isFalse,
+              reason: '$key should not be pressed with metaState $modifier with metaState $modifier '
+                  'and additional key ${RawKeyEventDataMacOs.modifierFunction}.',
+            );
+          }
+        }
+      }
+    });
+    test('Printable keyboard keys are correctly translated', () {
+      const String unmodifiedCharacter = 'a';
+      final RawKeyEvent keyAEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'macos',
+        'keyCode': 0x00000000,
+        'characters': 'a',
+        'charactersIgnoringModifiers': unmodifiedCharacter,
+        'modifiers': 0x0,
+      });
+      final RawKeyEventDataMacOs data = keyAEvent.data;
+      expect(data.physicalKey, equals(PhysicalKeyboardKey.keyA));
+      expect(data.logicalKey, equals(LogicalKeyboardKey.keyA));
+      expect(data.keyLabel, equals('a'));
+    });
+    test('Control keyboard keys are correctly translated', () {
+      final RawKeyEvent escapeKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'macos',
+        'keyCode': 0x00000035,
+        'characters': '',
+        'charactersIgnoringModifiers': '',
+        'character': null,
+        'modifiers': 0x0,
+      });
+      final RawKeyEventDataMacOs data = escapeKeyEvent.data;
+      expect(data.physicalKey, equals(PhysicalKeyboardKey.escape));
+      expect(data.logicalKey, equals(LogicalKeyboardKey.escape));
+      expect(data.keyLabel, isNull);
+    });
+    test('Modifier keyboard keys are correctly translated', () {
+      final RawKeyEvent shiftLeftKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
+        'type': 'keydown',
+        'keymap': 'macos',
+        'keyCode': 0x00000038,
+        'characters': '',
+        'charactersIgnoringModifiers': '',
+        'character': null,
+        'modifiers': RawKeyEventDataMacOs.modifierLeftShift,
+      });
+      final RawKeyEventDataMacOs data = shiftLeftKeyEvent.data;
+      expect(data.physicalKey, equals(PhysicalKeyboardKey.shiftLeft));
+      expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
+      expect(data.keyLabel, isNull);
+    });
+  });
 }