diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm index dd65f2e97285c..1a0747b3b59be 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm @@ -29,23 +29,15 @@ static NSUInteger lowestSetBit(NSUInteger bitmask) { /** * Whether a string represents a control character. */ -static bool IsControlCharacter(NSUInteger length, NSString* label) { - if (length > 1) { - return false; - } - unichar codeUnit = [label characterAtIndex:0]; - return (codeUnit <= 0x1f && codeUnit >= 0x00) || (codeUnit >= 0x7f && codeUnit <= 0x9f); +static bool IsControlCharacter(uint64_t character) { + return (character <= 0x1f && character >= 0x00) || (character >= 0x7f && character <= 0x9f); } /** * Whether a string represents an unprintable key. */ -static bool IsUnprintableKey(NSUInteger length, NSString* label) { - if (length > 1) { - return false; - } - unichar codeUnit = [label characterAtIndex:0]; - return codeUnit >= 0xF700 && codeUnit <= 0xF8FF; +static bool IsUnprintableKey(uint64_t character) { + return character >= 0xF700 && character <= 0xF8FF; } /** @@ -113,6 +105,40 @@ static uint64_t toLower(uint64_t n) { return n; } +// Decode a UTF-16 sequence to an array of char32 (UTF-32). +// +// See https://en.wikipedia.org/wiki/UTF-16#Description for the algorithm. +// +// The returned character array must be deallocated with delete[]. The length of +// the result is stored in `out_length`. +// +// Although NSString has a dataUsingEncoding method, we implement our own +// because dataUsingEncoding outputs redundant characters for unknown reasons. +static uint32_t* DecodeUtf16(NSString* target, size_t* out_length) { + // The result always has a length less or equal to target. + size_t result_pos = 0; + uint32_t* result = new uint32_t[target.length]; + uint16_t high_surrogate = 0; + for (NSUInteger target_pos = 0; target_pos < target.length; target_pos += 1) { + uint16_t codeUnit = [target characterAtIndex:target_pos]; + // BMP + if (codeUnit <= 0xD7FF || codeUnit >= 0xE000) { + result[result_pos] = codeUnit; + result_pos += 1; + // High surrogates + } else if (codeUnit <= 0xDBFF) { + high_surrogate = codeUnit - 0xD800; + // Low surrogates + } else { + uint16_t low_surrogate = codeUnit - 0xDC00; + result[result_pos] = (high_surrogate << 10) + low_surrogate + 0x10000; + result_pos += 1; + } + } + *out_length = result_pos; + return result; +} + /** * Returns the logical key of a KeyUp or KeyDown event. * @@ -125,30 +151,34 @@ static uint64_t GetLogicalKeyForEvent(NSEvent* event, uint64_t physicalKey) { return fromKeyCode.unsignedLongLongValue; } - NSString* keyLabel = event.charactersIgnoringModifiers; - NSUInteger keyLabelLength = [keyLabel length]; - // If this key is printable, generate the logical key from its Unicode - // value. Control keys such as ESC, CTRL, and SHIFT are not printable. HOME, - // DEL, arrow keys, and function keys are considered modifier function keys, - // which generate invalid Unicode scalar values. - if (keyLabelLength != 0 && !IsControlCharacter(keyLabelLength, keyLabel) && - !IsUnprintableKey(keyLabelLength, keyLabel)) { - // Given that charactersIgnoringModifiers can contain a string of arbitrary - // length, limit to a maximum of two Unicode scalar values. It is unlikely - // that a keyboard would produce a code point bigger than 32 bits, but it is - // still worth defending against this case. - NSCAssert((keyLabelLength < 2), @"Unexpected long key label: |%@|.", keyLabel); - - uint64_t codeUnit = (uint64_t)[keyLabel characterAtIndex:0]; - if (keyLabelLength == 2) { - uint64_t secondCode = (uint64_t)[keyLabel characterAtIndex:1]; - codeUnit = (codeUnit << 16) | secondCode; + // Convert `charactersIgnoringModifiers` to UTF32. + NSString* keyLabelUtf16 = event.charactersIgnoringModifiers; + + // Check if this key is a single character, which will be used to generate the + // logical key from its Unicode value. + // + // Multi-char keys will be minted onto the macOS plane because there are no + // meaningful values for them. Control keys and unprintable keys have been + // converted by `keyCodeToLogicalKey` earlier. + uint32_t character = 0; + if (keyLabelUtf16.length != 0) { + size_t keyLabelLength; + uint32_t* keyLabel = DecodeUtf16(keyLabelUtf16, &keyLabelLength); + if (keyLabelLength == 1) { + uint32_t keyLabelChar = *keyLabel; + delete[] keyLabel; + NSCAssert(!IsControlCharacter(keyLabelChar) && !IsUnprintableKey(keyLabelChar), + @"Unexpected control or unprintable keylabel 0x%x", keyLabelChar); + NSCAssert(keyLabelChar <= 0x10FFFF, @"Out of range keylabel 0x%x", keyLabelChar); + character = keyLabelChar; } - return KeyOfPlane(toLower(codeUnit), kUnicodePlane); + } + if (character != 0) { + return KeyOfPlane(toLower(character), kUnicodePlane); } - // This is a non-printable key that is unrecognized, so a new code is minted - // to the macOS plane. + // We can't represent this key with a single printable unicode, so a new code + // is minted to the macOS plane. return KeyOfPlane(event.keyCode, kMacosPlane); } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm index 7e1012569abb6..f61f1c99308b2 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm @@ -265,6 +265,47 @@ - (void)dealloc { [events removeAllObjects]; } +TEST(FlutterEmbedderKeyResponderUnittests, MultipleCharacters) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0, @"àn", @"àn", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, 0x1400000000ull); + EXPECT_STREQ(event->character, "àn"); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, 0x1400000000ull); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; +} + TEST(FlutterEmbedderKeyResponderUnittests, IgnoreDuplicateDownEvent) { __block NSMutableArray* events = [[NSMutableArray alloc] init]; __block BOOL last_handled = TRUE;