From ecf1cce804378f3d62c8f238bef58be4f737b08c Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Wed, 5 Aug 2026 09:51:57 -0700 Subject: [PATCH] =?UTF-8?q?fix(osr):=20give=20the=20page=20e.key=20back=20?= =?UTF-8?q?=E2=80=94=20a=20zero=20character=20pair=20is=20FlagsChanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every printable keydown reached the page as `e.key === "Unidentified"`, so no page-level keyboard shortcut worked in an OSR webview (⌘F, ⌘K, j/k navigation). `e.code` was correct throughout, which made it look like a partial keyboard rather than a broken one. The cause is not a missing `dom_key`. CEF never sets `dom_key` on macOS at all — `TranslateWebKeyEvent` builds a synthetic NSEvent and lets Chromium's `NativeWebKeyboardEvent` derive it. But its first branch is: if (key_event.character == 0 && key_event.unmodified_character == 0) event_type = NSEventTypeFlagsChanged; and `DomKeyFromNSEvent` answers a FlagsChanged event with `DomKeyFromKeyCode()`, which maps only the modifier keys. We sent 0 for every printable key — `kCefMacKeyChars` held editing/navigation keys only — so each one was delivered as a modifier-state change carrying no modifier. `DomCodeFromNSEvent` reads `keyCode` whatever the event type, which is exactly why `e.code` survived and hid the shape of the bug. So `cefMacCharForKey` now falls back to the key's own printable codepoint (`LogicalKeyboardKey.keyId` is the lowercase Unicode value and follows the active layout, so it is the character macOS would report), and the call site prefers the typed character when there is one — a shifted key reports `A` like a browser rather than the unshifted `a`. The function-row keys get their NSEvent private-use codepoints for the same reason. Modifier keys stay 0: FlagsChanged is the correct event for them, and it is the one case `DomKeyFromKeyCode` does map. Dart-side only — `main.mm` already forwards `character` into both CefKeyEvent character fields, so no protocol change and no cef_host rebuild. Follow-up, not needed for this fix: `unmodified_character` is currently a copy of `character`, and `is_system_key` is never sent at all. Carrying both properly needs two new wire fields and a host rebuild. --- lib/src/cef_web_view.dart | 20 +++++++--- .../lib/src/cef_input.dart | 39 +++++++++++++++++-- test/cef_input_test.dart | 30 ++++++++++++-- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/lib/src/cef_web_view.dart b/lib/src/cef_web_view.dart index 462ddd5..f141ff8 100644 --- a/lib/src/cef_web_view.dart +++ b/lib/src/cef_web_view.dart @@ -597,12 +597,20 @@ class _CefWebViewState extends State _isWindows ? wkc : (cefMacNativeKeyCode(event.physicalKey) ?? wkc); final ch = event.character; final isText = ch != null && _isPrintable(ch); - // Editing / navigation keys MUST carry the macOS NSEvent character or CEF - // OSR double-applies them (one Backspace deletes two, one arrow moves two). - // macOS-only: the table holds NSEvent codepoints (incl. private-use - // 0xF7xx function-key values) that would corrupt a Windows key event — - // Windows CEF derives the character from the VK itself, so send 0 there. - final keyChar = _isWindows ? 0 : cefMacCharForKey(event.logicalKey); + // Every key MUST carry its macOS NSEvent character. Editing/navigation keys + // because CEF OSR otherwise double-applies them (one Backspace deletes two, + // one arrow moves two); printable keys because a zero character pair makes + // CEF build a FlagsChanged event, which costs the page `e.key` entirely — + // see [cefMacCharForKey]. The typed character wins when there is one, so a + // shifted key reports `A` like a browser rather than the unshifted `a`. + // macOS-only: these are NSEvent codepoints (incl. private-use 0xF7xx + // function-key values) that would corrupt a Windows key event — Windows CEF + // derives the character from the VK itself, so send 0 there. + final keyChar = _isWindows + ? 0 + : (ch != null && isText + ? ch.codeUnitAt(0) + : cefMacCharForKey(event.logicalKey)); // The page should see keydown→keypress→keyup for every character, like a // browser. We always send RAWKEYDOWN/KEYUP; the keypress (CHAR) is // synthesized by [_commitText] when the IME's insertText delivers a typed diff --git a/packages/flutter_cef_platform_interface/lib/src/cef_input.dart b/packages/flutter_cef_platform_interface/lib/src/cef_input.dart index fe152e0..e1b7ede 100644 --- a/packages/flutter_cef_platform_interface/lib/src/cef_input.dart +++ b/packages/flutter_cef_platform_interface/lib/src/cef_input.dart @@ -169,11 +169,44 @@ final Map kCefMacKeyChars = { LogicalKeyboardKey.end: 0xF72B, LogicalKeyboardKey.pageUp: 0xF72C, LogicalKeyboardKey.pageDown: 0xF72D, + LogicalKeyboardKey.f1: 0xF704, // NSF1FunctionKey … NSF12FunctionKey + LogicalKeyboardKey.f2: 0xF705, + LogicalKeyboardKey.f3: 0xF706, + LogicalKeyboardKey.f4: 0xF707, + LogicalKeyboardKey.f5: 0xF708, + LogicalKeyboardKey.f6: 0xF709, + LogicalKeyboardKey.f7: 0xF70A, + LogicalKeyboardKey.f8: 0xF70B, + LogicalKeyboardKey.f9: 0xF70C, + LogicalKeyboardKey.f10: 0xF70D, + LogicalKeyboardKey.f11: 0xF70E, + LogicalKeyboardKey.f12: 0xF70F, }; -/// The macOS NSEvent character for an editing/navigation [key], or 0 for keys -/// whose text rides the IME/CHAR path. See [kCefMacKeyChars]. -int cefMacCharForKey(LogicalKeyboardKey key) => kCefMacKeyChars[key] ?? 0; +/// The macOS NSEvent character for [key]: the table above, then the key's own +/// printable codepoint. +/// +/// A printable key must carry a character too — 0 for *both* `character` and +/// `unmodified_character` does not mean "no character" to CEF, it means a +/// different event. `TranslateWebKeyEvent` treats that pair as +/// `NSEventTypeFlagsChanged`, and Chromium's `DomKeyFromNSEvent` answers a +/// FlagsChanged event with `DomKeyFromKeyCode()`, which maps only the modifier +/// keys — so every printable keydown reached the page as +/// `e.key === 'Unidentified'`, breaking any page-level shortcut. (`e.code` +/// survived because `DomCodeFromNSEvent` reads `keyCode` whatever the type, +/// which is why the symptom looked like a partial keyboard rather than none.) +/// +/// [LogicalKeyboardKey.keyId] is the lowercase Unicode value for a character +/// key and follows the active layout, so it is exactly the unmodified +/// character macOS would report. +int cefMacCharForKey(LogicalKeyboardKey key) { + final mapped = kCefMacKeyChars[key]; + if (mapped != null) return mapped; + final id = key.keyId; + // Above the Unicode range are Flutter's synthetic plane ids (modifiers, media + // keys); those genuinely have no character and stay 0. + return (id > 0x20 && id < 0x7F) || (id > 0x7F && id <= 0x10FFFF) ? id : 0; +} /// The Windows virtual-key code for [key]: the special-key table first, then /// a→VK_A..z→VK_Z, A–Z, and 0–9. 0 if unmapped (a printable that rides CHAR). diff --git a/test/cef_input_test.dart b/test/cef_input_test.dart index 6bb1250..f7792f8 100644 --- a/test/cef_input_test.dart +++ b/test/cef_input_test.dart @@ -71,9 +71,33 @@ void main() { expect(cefMacCharForKey(key), isNonZero); // the whole point }); }); - test('printable keys carry no override char (-> 0; they ride the IME)', () { - expect(cefMacCharForKey(LogicalKeyboardKey.keyA), 0); - expect(cefMacCharForKey(LogicalKeyboardKey.digit5), 0); + // A printable key must carry its character too, though it rides the IME for + // text. Zero for BOTH character fields is not "no character" to CEF: it is + // the FlagsChanged branch of TranslateWebKeyEvent, and Chromium answers a + // FlagsChanged event with DomKeyFromKeyCode(), which maps only modifiers — + // so the page saw `e.key === 'Unidentified'` for every printable keydown. + test('printable keys carry their own codepoint (e.key, not Unidentified)', + () { + expect(cefMacCharForKey(LogicalKeyboardKey.keyA), 0x61); // 'a' + expect(cefMacCharForKey(LogicalKeyboardKey.digit5), 0x35); // '5' + expect(cefMacCharForKey(LogicalKeyboardKey.slash), 0x2F); // '/' + }); + test('function keys carry their NSEvent private-use codepoint', () { + expect(cefMacCharForKey(LogicalKeyboardKey.f1), 0xF704); + expect(cefMacCharForKey(LogicalKeyboardKey.f12), 0xF70F); + }); + // Modifiers are the one case where FlagsChanged is the CORRECT event, and + // DomKeyFromKeyCode does map them — so these must stay 0. + test('modifier keys stay 0 — FlagsChanged is right for them', () { + for (final key in [ + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.metaLeft, + LogicalKeyboardKey.capsLock, + ]) { + expect(cefMacCharForKey(key), 0, reason: key.debugName); + } }); });