From cc9b78fc5c4a4d2d51316d9626523336230a89a9 Mon Sep 17 00:00:00 2001 From: Ferhat Date: Thu, 25 Feb 2021 13:26:03 -0800 Subject: [PATCH] [web] Treeshake keymaps for web (4% code size reduction in hello world) (#75945) --- .../lib/src/services/raw_keyboard.dart | 176 ++++++++++-------- .../test/services/raw_keyboard_test.dart | 137 +++++++++++--- .../widgets/raw_keyboard_listener_test.dart | 31 +++ .../lib/src/event_simulation.dart | 126 +++++++------ 4 files changed, 309 insertions(+), 161 deletions(-) diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart index 41e927ac8e..bcddb0dd5a 100644 --- a/packages/flutter/lib/src/services/raw_keyboard.dart +++ b/packages/flutter/lib/src/services/raw_keyboard.dart @@ -267,92 +267,106 @@ abstract class RawKeyEvent with Diagnosticable { String? character; final String keymap = message['keymap'] as String; - switch (keymap) { - case 'android': - data = RawKeyEventDataAndroid( - flags: message['flags'] as int? ?? 0, - codePoint: message['codePoint'] as int? ?? 0, - keyCode: message['keyCode'] as int? ?? 0, - plainCodePoint: message['plainCodePoint'] as int? ?? 0, - scanCode: message['scanCode'] as int? ?? 0, - metaState: message['metaState'] as int? ?? 0, - eventSource: message['source'] as int? ?? 0, - vendorId: message['vendorId'] as int? ?? 0, - productId: message['productId'] as int? ?? 0, - deviceId: message['deviceId'] as int? ?? 0, - repeatCount: message['repeatCount'] as int? ?? 0, - ); - if (message.containsKey('character')) { - character = message['character'] as String?; - } - break; - case 'fuchsia': - final int codePoint = message['codePoint'] as int? ?? 0; - data = RawKeyEventDataFuchsia( - hidUsage: message['hidUsage'] as int? ?? 0, - codePoint: codePoint, - modifiers: message['modifiers'] as int? ?? 0, - ); - if (codePoint != 0) { - character = String.fromCharCode(codePoint); - } - break; - case 'macos': - data = RawKeyEventDataMacOs( - characters: message['characters'] as String? ?? '', - charactersIgnoringModifiers: message['charactersIgnoringModifiers'] as String? ?? '', + if (kIsWeb) { + final String? key = message['key'] as String?; + data = RawKeyEventDataWeb( + code: message['code'] as String? ?? '', + key: key ?? '', + metaState: message['metaState'] as int? ?? 0, + ); + if (key != null && key.isNotEmpty) { + character = key; + } + } else { + switch (keymap) { + case 'android': + data = RawKeyEventDataAndroid( + flags: message['flags'] as int? ?? 0, + codePoint: message['codePoint'] as int? ?? 0, keyCode: message['keyCode'] as int? ?? 0, - modifiers: message['modifiers'] as int? ?? 0); - character = message['characters'] as String?; - break; - case 'ios': - data = RawKeyEventDataIos( - characters: message['characters'] as String? ?? '', - charactersIgnoringModifiers: message['charactersIgnoringModifiers'] as String? ?? '', - keyCode: message['keyCode'] as int? ?? 0, - modifiers: message['modifiers'] as int? ?? 0); - break; - case 'linux': - final int unicodeScalarValues = message['unicodeScalarValues'] as int? ?? 0; - data = RawKeyEventDataLinux( - keyHelper: KeyHelper(message['toolkit'] as String? ?? ''), - unicodeScalarValues: unicodeScalarValues, + plainCodePoint: message['plainCodePoint'] as int? ?? 0, + scanCode: message['scanCode'] as int? ?? 0, + metaState: message['metaState'] as int? ?? 0, + eventSource: message['source'] as int? ?? 0, + vendorId: message['vendorId'] as int? ?? 0, + productId: message['productId'] as int? ?? 0, + deviceId: message['deviceId'] as int? ?? 0, + repeatCount: message['repeatCount'] as int? ?? 0, + ); + if (message.containsKey('character')) { + character = message['character'] as String?; + } + break; + case 'fuchsia': + final int codePoint = message['codePoint'] as int? ?? 0; + data = RawKeyEventDataFuchsia( + hidUsage: message['hidUsage'] as int? ?? 0, + codePoint: codePoint, + modifiers: message['modifiers'] as int? ?? 0, + ); + if (codePoint != 0) { + character = String.fromCharCode(codePoint); + } + break; + case 'macos': + data = RawKeyEventDataMacOs( + characters: message['characters'] as String? ?? '', + charactersIgnoringModifiers: message['charactersIgnoringModifiers'] as String? ?? '', + keyCode: message['keyCode'] as int? ?? 0, + modifiers: message['modifiers'] as int? ?? 0); + character = message['characters'] as String?; + break; + case 'ios': + data = RawKeyEventDataIos( + characters: message['characters'] as String? ?? '', + charactersIgnoringModifiers: message['charactersIgnoringModifiers'] as String? ?? '', + keyCode: message['keyCode'] as int? ?? 0, + modifiers: message['modifiers'] as int? ?? 0); + break; + case 'linux': + final int unicodeScalarValues = message['unicodeScalarValues'] as int? ?? 0; + data = RawKeyEventDataLinux( + keyHelper: KeyHelper(message['toolkit'] as String? ?? ''), + unicodeScalarValues: unicodeScalarValues, + keyCode: message['keyCode'] as int? ?? 0, + scanCode: message['scanCode'] as int? ?? 0, + modifiers: message['modifiers'] as int? ?? 0, + isDown: message['type'] == 'keydown'); + if (unicodeScalarValues != 0) { + character = String.fromCharCode(unicodeScalarValues); + } + break; + case 'windows': + final int characterCodePoint = message['characterCodePoint'] as int? ?? 0; + data = RawKeyEventDataWindows( keyCode: message['keyCode'] as int? ?? 0, scanCode: message['scanCode'] as int? ?? 0, + characterCodePoint: characterCodePoint, modifiers: message['modifiers'] as int? ?? 0, - isDown: message['type'] == 'keydown'); - if (unicodeScalarValues != 0) { - character = String.fromCharCode(unicodeScalarValues); - } - break; - case 'web': - data = RawKeyEventDataWeb( - code: message['code'] as String? ?? '', - key: message['key'] as String? ?? '', - metaState: message['metaState'] as int? ?? 0, - ); - character = message['key'] as String?; - break; - case 'windows': - final int characterCodePoint = message['characterCodePoint'] as int? ?? 0; - data = RawKeyEventDataWindows( - keyCode: message['keyCode'] as int? ?? 0, - scanCode: message['scanCode'] as int? ?? 0, - characterCodePoint: characterCodePoint, - modifiers: message['modifiers'] as int? ?? 0, - ); - if (characterCodePoint != 0) { - character = String.fromCharCode(characterCodePoint); - } - break; - default: - /// This exception would only be hit on platforms that haven't yet - /// implemented raw key events, but will only be triggered if the - /// engine for those platforms sends raw key event messages in the - /// first place. - throw FlutterError('Unknown keymap for key events: $keymap'); + ); + if (characterCodePoint != 0) { + character = String.fromCharCode(characterCodePoint); + } + break; + case 'web': + final String? key = message['key'] as String?; + data = RawKeyEventDataWeb( + code: message['code'] as String? ?? '', + key: key ?? '', + metaState: message['metaState'] as int? ?? 0, + ); + if (key != null && key.isNotEmpty) { + character = key; + } + break; + default: + /// This exception would only be hit on platforms that haven't yet + /// implemented raw key events, but will only be triggered if the + /// engine for those platforms sends raw key event messages in the + /// first place. + throw FlutterError('Unknown keymap for key events: $keymap'); + } } - final String type = message['type'] as String; switch (type) { case 'keydown': diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart index 2e0b3ef52d..1b40acb17e 100644 --- a/packages/flutter/test/services/raw_keyboard_test.dart +++ b/packages/flutter/test/services/raw_keyboard_test.dart @@ -31,7 +31,7 @@ void main() { } }); testWidgets('No character is produced for non-printables', (WidgetTester tester) async { - for (final String platform in ['linux', 'android', 'macos', 'fuchsia', 'windows']) { + for (final String platform in ['linux', 'android', 'macos', 'fuchsia', 'windows', 'web']) { void handleKey(RawKeyEvent event) { expect(event.character, isNull, reason: 'on $platform'); } @@ -194,7 +194,7 @@ void main() { await simulateKeyUpEvent(LogicalKeyboardKey.keyA, platform: platform, physicalKey: PhysicalKeyboardKey.keyA); expect(RawKeyboard.instance.keysPressed, isEmpty, reason: 'on $platform'); } - }); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/76741 testWidgets('keysPressed modifiers are synchronized with key events on macOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -219,7 +219,7 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }); + }, skip: isBrowser); // This is a macOS-specific test. testWidgets('keysPressed modifiers are synchronized with key events on iOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -244,7 +244,7 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }); + }, skip: isBrowser); // This is an iOS-specific test. testWidgets('keysPressed modifiers are synchronized with key events on Windows', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -269,7 +269,7 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }); + }, skip: isBrowser); // This is a Windows-specific test. testWidgets('keysPressed modifiers are synchronized with key events on android', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -294,7 +294,7 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }); + }, skip: isBrowser); // This is an Android-specific test. testWidgets('keysPressed modifiers are synchronized with key events on fuchsia', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -319,7 +319,7 @@ void main() { {LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.keyA}, ), ); - }); + }, skip: isBrowser); // This is a Fuchsia-specific test. testWidgets('keysPressed modifiers are synchronized with key events on Linux GLFW', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -350,6 +350,37 @@ void main() { }, ), ); + }, skip: isBrowser); // This is a GLFW-specific test. + + testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async { + expect(RawKeyboard.instance.keysPressed, isEmpty); + // Generate the data for a regular key down event. + final Map data = KeyEventSimulator.getKeyData( + LogicalKeyboardKey.keyA, + platform: 'web', + isDown: true, + ); + // Change the modifiers so that they show the shift key as already down + // when this event is received, but it's not in keysPressed yet. + data['metaState'] |= RawKeyEventDataWeb.modifierShift; + // dispatch the modified data. + await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + SystemChannels.keyEvent.name, + SystemChannels.keyEvent.codec.encodeMessage(data), + (ByteData? data) {}, + ); + expect( + RawKeyboard.instance.keysPressed, + equals( + { + LogicalKeyboardKey.shiftLeft, + // Web doesn't distinguish between left and right keys, so they're + // all shown as down when either is pressed. + LogicalKeyboardKey.shiftRight, + LogicalKeyboardKey.keyA, + }, + ), + ); }); testWidgets('sided modifiers without a side set return all sides on Android', (WidgetTester tester) async { @@ -388,7 +419,7 @@ void main() { }, ), ); - }); + }, skip: isBrowser); // This is an Android-specific test. testWidgets('sided modifiers without a side set return all sides on macOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -426,7 +457,7 @@ void main() { }, ), ); - }); + }, skip: isBrowser); // This is a macOS-specific test. testWidgets('sided modifiers without a side set return all sides on iOS', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -464,7 +495,7 @@ void main() { }, ), ); - }); + }, skip: isBrowser); // This is an iOS-specific test. testWidgets('sided modifiers without a side set return all sides on Windows', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -500,7 +531,7 @@ void main() { }, ), ); - }); + }, skip: isBrowser); // This is a Windows-specific test. testWidgets('sided modifiers without a side set return all sides on Linux GLFW', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); @@ -539,6 +570,44 @@ void main() { }, ), ); + }, skip: isBrowser); // This is a GLFW-specific test. + + testWidgets('sided modifiers without a side set return all sides on web', (WidgetTester tester) async { + expect(RawKeyboard.instance.keysPressed, isEmpty); + // Generate the data for a regular key down event. + final Map data = KeyEventSimulator.getKeyData( + LogicalKeyboardKey.keyA, + platform: 'web', + isDown: true, + ); + // Set only the generic "shift down" modifier, without setting a side. + data['metaState'] |= + RawKeyEventDataWeb.modifierShift | + RawKeyEventDataWeb.modifierAlt | + RawKeyEventDataWeb.modifierControl | + RawKeyEventDataWeb.modifierMeta; + // dispatch the modified data. + await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + SystemChannels.keyEvent.name, + SystemChannels.keyEvent.codec.encodeMessage(data), + (ByteData? data) {}, + ); + expect( + RawKeyboard.instance.keysPressed, + equals( + { + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.shiftRight, + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.altRight, + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.controlRight, + LogicalKeyboardKey.metaLeft, + LogicalKeyboardKey.metaRight, + LogicalKeyboardKey.keyA, + }, + ), + ); }); testWidgets('RawKeyboard asserts if no keys are in keysPressed after receiving a key down event', (WidgetTester tester) async { @@ -547,19 +616,32 @@ void main() { FlutterError.onError = (FlutterErrorDetails details) { errorDetails = details; }; + + final Map keyEventMessage; + if (kIsWeb) { + keyEventMessage = const { + 'type': 'keydown', + 'keymap': 'web', + 'code': 'ShiftLeft', // Left shift code + 'metaState': 0x0, // No shift key metaState set! + }; + } else { + keyEventMessage = const { + 'type': 'keydown', + 'keymap': 'android', + 'keyCode': 0x3b, // Left shift key keyCode + 'scanCode': 0x2a, + 'metaState': 0x0, // No shift key metaState set! + 'source': 0x101, + 'deviceId': 1, + }; + } + try { await ServicesBinding.instance!.defaultBinaryMessenger .handlePlatformMessage( SystemChannels.keyEvent.name, - SystemChannels.keyEvent.codec.encodeMessage(const { - 'type': 'keydown', - 'keymap': 'android', - 'keyCode': 0x3b, // Left shift key keyCode - 'scanCode': 0x2a, - 'metaState': 0x0, // No shift key metaState set! - 'source': 0x101, - 'deviceId': 1, - }), + SystemChannels.keyEvent.codec.encodeMessage(keyEventMessage), (ByteData? data) {}, ); } finally { @@ -837,7 +919,8 @@ void main() { expect(message, equals({ 'handled': true })); ServicesBinding.instance!.defaultBinaryMessenger.setMockMessageHandler(SystemChannels.keyEvent.name, null); }); - }); + }, skip: isBrowser); // This is an Android-specific group. + group('RawKeyEventDataFuchsia', () { const Map modifierTests = { RawKeyEventDataFuchsia.modifierAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.any), @@ -951,7 +1034,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft)); expect(data.keyLabel, isEmpty); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347 - }); + }, skip: isBrowser); // This is a Fuchsia-specific group. group('RawKeyEventDataMacOs', () { const Map modifierTests = { @@ -1097,7 +1180,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft)); expect(data.logicalKey.keyLabel, isEmpty); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347 - }); + }, skip: isBrowser); // This is a macOS-specific group. group('RawKeyEventDataIos', () { const Map modifierTests = { @@ -1243,7 +1326,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft)); expect(data.logicalKey.keyLabel, isEmpty); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/35347 - }); + }, skip: isBrowser); // This is an iOS-specific group. group('RawKeyEventDataWindows', () { const Map modifierTests = { @@ -1388,7 +1471,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft)); expect(data.logicalKey.keyLabel, isEmpty); }); - }); + }, skip: isBrowser); // This is a Windows-specific group. group('RawKeyEventDataLinux-GFLW', () { const Map modifierTests = { @@ -1572,7 +1655,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft)); expect(data.keyLabel, isEmpty); }); - }); + }, skip: isBrowser); // This is a GLFW-specific group. group('RawKeyEventDataLinux-GTK', () { const Map modifierTests = { @@ -1756,7 +1839,7 @@ void main() { expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft)); expect(data.keyLabel, isEmpty); }); - }); + }, skip: isBrowser); // This is a GTK-specific group. group('RawKeyEventDataWeb', () { const Map modifierTests = { diff --git a/packages/flutter/test/widgets/raw_keyboard_listener_test.dart b/packages/flutter/test/widgets/raw_keyboard_listener_test.dart index bcac58dc55..3149e0dc8c 100644 --- a/packages/flutter/test/widgets/raw_keyboard_listener_test.dart +++ b/packages/flutter/test/widgets/raw_keyboard_listener_test.dart @@ -42,6 +42,37 @@ void main() { expect(typedData.modifiers, RawKeyEventDataFuchsia.modifierLeftMeta); expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue); + await tester.pumpWidget(Container()); + focusNode.dispose(); + }, skip: isBrowser); // This is a Fuchsia-specific test. + + testWidgets('Web key event', (WidgetTester tester) async { + final List events = []; + + final FocusNode focusNode = FocusNode(); + + await tester.pumpWidget( + RawKeyboardListener( + focusNode: focusNode, + onKey: events.add, + child: Container(), + ), + ); + + focusNode.requestFocus(); + await tester.idle(); + + await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'web'); + await tester.idle(); + + expect(events.length, 2); + expect(events[0].runtimeType, equals(RawKeyDownEvent)); + expect(events[0].data, isA()); + final RawKeyEventDataWeb typedData = events[0].data as RawKeyEventDataWeb; + expect(typedData.code, 'MetaLeft'); + expect(typedData.metaState, RawKeyEventDataWeb.modifierMeta); + expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue); + await tester.pumpWidget(Container()); focusNode.dispose(); }); diff --git a/packages/flutter_test/lib/src/event_simulation.dart b/packages/flutter_test/lib/src/event_simulation.dart index 82451d2fcb..b99dab818c 100644 --- a/packages/flutter_test/lib/src/event_simulation.dart +++ b/packages/flutter_test/lib/src/event_simulation.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart'; import 'test_async_utils.dart'; @@ -85,40 +86,47 @@ class KeyEventSimulator { static int _getKeyCode(LogicalKeyboardKey key, String platform) { assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); - late Map map; - switch (platform) { - case 'android': - map = kAndroidToLogicalKey; - break; - case 'fuchsia': - map = kFuchsiaToLogicalKey; - break; - case 'macos': - // macOS doesn't do key codes, just scan codes. - return -1; - case 'ios': - // iOS doesn't do key codes, just scan codes. - return -1; - case 'web': - // web doesn't have int type code - return -1; - case 'linux': - map = kGlfwToLogicalKey; - break; - case 'windows': - map = kWindowsToLogicalKey; - break; - } - int? keyCode; - for (final int code in map.keys) { - if (key.keyId == map[code]!.keyId) { - keyCode = code; - break; + if (kIsWeb) { + // web doesn't have int type code. This check is used to treeshake + // keyboard map code. + return -1; + } else { + late Map map; + switch (platform) { + case 'android': + map = kAndroidToLogicalKey; + break; + case 'fuchsia': + map = kFuchsiaToLogicalKey; + break; + case 'macos': + // macOS doesn't do key codes, just scan codes. + return -1; + case 'ios': + // iOS doesn't do key codes, just scan codes. + return -1; + case 'web': + // web doesn't have int type code. + return -1; + case 'linux': + map = kGlfwToLogicalKey; + break; + case 'windows': + map = kWindowsToLogicalKey; + break; } + int? keyCode; + for (final int code in map.keys) { + if (key.keyId == map[code]!.keyId) { + keyCode = code; + break; + } + } + assert(keyCode != null, 'Key $key not found in $platform keyCode map'); + return keyCode!; } - assert(keyCode != null, 'Key $key not found in $platform keyCode map'); - return keyCode!; } + static String _getWebKeyCode(LogicalKeyboardKey key) { String? result; for (final String code in kWebToLogicalKey.keys) { @@ -134,28 +142,33 @@ class KeyEventSimulator { static PhysicalKeyboardKey _findPhysicalKey(LogicalKeyboardKey key, String platform) { assert(_osIsSupported(platform), 'Platform $platform not supported for key simulation'); late Map map; - switch (platform) { - case 'android': - map = kAndroidToPhysicalKey; - break; - case 'fuchsia': - map = kFuchsiaToPhysicalKey; - break; - case 'macos': - map = kMacOsToPhysicalKey; - break; - case 'ios': - map = kIosToPhysicalKey; - break; - case 'linux': - map = kLinuxToPhysicalKey; - break; - case 'web': - map = kWebToPhysicalKey; - break; - case 'windows': - map = kWindowsToPhysicalKey; - break; + if (kIsWeb) { + // This check is used to treeshake keymap code. + map = kWebToPhysicalKey; + } else { + switch (platform) { + case 'android': + map = kAndroidToPhysicalKey; + break; + case 'fuchsia': + map = kFuchsiaToPhysicalKey; + break; + case 'macos': + map = kMacOsToPhysicalKey; + break; + case 'ios': + map = kIosToPhysicalKey; + break; + case 'linux': + map = kLinuxToPhysicalKey; + break; + case 'web': + map = kWebToPhysicalKey; + break; + case 'windows': + map = kWindowsToPhysicalKey; + break; + } } PhysicalKeyboardKey? result; for (final PhysicalKeyboardKey physicalKey in map.values) { @@ -191,6 +204,13 @@ class KeyEventSimulator { 'keymap': platform, }; + if (kIsWeb) { + result['code'] = _getWebKeyCode(key); + result['key'] = key.keyLabel; + result['metaState'] = _getWebModifierFlags(key, isDown); + return result; + } + switch (platform) { case 'android': result['keyCode'] = keyCode;