Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[Web, keyboard] Locale layout mapping #34625

Merged
merged 91 commits into from
Nov 30, 2022

Conversation

dkwingsmt
Copy link
Contributor

@dkwingsmt dkwingsmt commented Jul 13, 2022

This PR changes how logical keys are mapped on Web, aligning it with other platforms. Part of flutter/flutter#100456.

This change ensures that non-latin layout can also use letter shortcuts. For example, before this change, on Russian layout, physical key Ctrl-KeyA will produce logical key Ctrl-ф, which will not trigger any common shortcuts. After the change, it will produce logical key Ctrl-A.

Technical Details

The algorithm is as follows: Analyze every key of the current layout,

  1. If a key can produce an alnum under some modifier, then this key is mapped to this alnum.
  2. After the previous step, if some alnum is not mapped, they're mapped to their corresponding key on the US keyboard.
  3. The remaining keys are mapped to the unicode plane according to their produced character.

This algorithm, which we call the benchmark planner, has been applied to all other desktop platforms.

However, we can't simply apply it to Web: Web DOM API does not tell which keyboard layout the user is on, or how the current layout maps keys (there is a KeyboardLayout API that is supported only by Chrome, and explicitly refused by all other browsers). So we have to invent a "blind" algorithm that applies to any layout, while keeping the same result.

Luckily, we're able to fetch a list of "all keyboard layouts" from Microsoft/VSCode repo, and we analyzed all layouts beforehand, and managed to combine the result into a huge code -> key -> result map. You would imagine it being impossible, since different layouts might use the same (code, key) pair for different characters, but in fact such conflicts are surprisingly few, and all the conflicts are mapped to letters. For example, es-linux maps ('KeyY', '←') to y, while de-linux maps ('KeyY', '←') to z.

We can't distinguished these conflicts only by the (code, key) pair, but we can use other information: keyCode. Now, keyCode is a deprecated property, but we really don't see it being removed any time foreseeable. Also, although keyCode is infamous for being platform-dependent, for letter keys it is always equal to the letter character. Therefore such conflicting cases are all mapped to a special value, 0x01, indicating "use keyCode".

Moreover, to reduce the size of the map, we noticed there are certain patterns that can be easily represented by some if statements. These patterns are extracted as the so-called "heuristic mapper". This reduces the map from over 1600 entries to ~450 entries.

To further reduce the package size overhead, this PR also encodes the map into a string that is decoded at run time. This reduces the package size over by ~45% at the cost of code complexity.

Structure

A package called gen_web_locale_keymap is added to tools/, which:

  1. Fetches all layouts from Microsoft/VSCode
  2. Compute the layout map
  3. Generate the resulting map data to web_locale_keymap (see below)
  4. Generate the test cases to web_locale_keymap
    • The test cases are an enumeration of all entries (of interest) of all layouts, comparing the result of the resulting mapper class produces the same result as the benchmark planner.

A package called web_locale_keymap is added to third_party, which does the mapping for all printable characters. Since package must be placed in third_party because it uses Microsoft's code (of MIT license).

The SDK rewriter and the license checker are modified to accommodate the new package.

Package size overhead

Compiling a very simple app that listens to key events:

main.dart.js Change
Before PR 2048730 bytes
After PR 2053058 bytes +4328 bytes (0.21%)

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on
    writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added the platform-web Code specifically for the web engine label Jul 13, 2022
@skia-gold
Copy link

Gold has detected about 1 new digest(s) on patchset 20.
View them at https://flutter-engine-gold.skia.org/cl/github/34625

@dkwingsmt
Copy link
Contributor Author

dkwingsmt commented Nov 28, 2022

@mdebbar I've sync'd the PR with the latest master, addressing as many of you comments as possible.

I've also made a non-trivial change that compresses the data, which reduces the package size overhead of this PR further (see PR body).

I'm still resolving some CI issues. In the mean time, can you take another look?


final DispatchKeyData performDispatchKeyData;
// Whether the current platform is macOS, which affects how certain key events
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the case that onMacOS is only used to differentiate things for macOS, or are these keyboard quirks also present on iOS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find. I've tested it on my iPad and I confirm that they also present on iOS. I'm changing it to onDarwin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me make it a separate PR after this PR.

result = result + _kCharLowerA - _kCharUpperA;
}
return result;
return isUpperLetter(key.codeUnitAt(0)) && key.length > 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't key.length > 1 sufficient? Or can non-named keys have more than one character? Would it be true that key.length > 2 would be enough? Just trying to see if we can avoid checking the code unit range entirely.

Copy link
Contributor Author

@dkwingsmt dkwingsmt Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point.

Although I'm not sure if there are any character KeyboardKey.keys that contain multiple JS characters, all keys that LocaleMapping handles are single-character, which is the most important aspect what this function cares about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this range check for now and revise it when we see a counterexample.

Copy link
Contributor Author

@dkwingsmt dkwingsmt Nov 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just did a quick check of the VSCode layouts. It turns out that en-in has a key which is two characters. Although, my previous algorithm also fails on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do all named keys have a name that is >2 characters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure so. Because all named keys are alnum, therefore having only 1 character conflicts with letter keys.

Also this is the standard for KeyboardEvent.key: https://w3c.github.io/uievents-key/ , which clearly says that key can consist of a letter and a combining code unit (accents).

@dkwingsmt
Copy link
Contributor Author

@gspencergoog I've addressed all your comments. I'd like to add another test for the en-in case, but I'm currently having a problem with felt test. Nevertheless, can you take another look?

Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some minor comments.

If you don't mind, I would like to wait for @eyebrowsoffire's input on the web_locale_keymap_sources thing in web_sdk/BUILD.gn. If it turns out it's needed, then I have to add web_unicode_sources too.

final String? character = logicalKeyIsCharacter ? eventKey : null;
final int logicalKey = () {
final bool logicalKeyIsCharacter = !_eventKeyIsKeyName(eventKey);
final ValueGetter<int> logicalKey = _cached<int>(() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for using _cached here instead of late final:

late final int logicalKey = () {
  // ...
}();

Copy link
Contributor Author

@dkwingsmt dkwingsmt Nov 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function might not be evaluated: if the event is a key up event, the logical key is simply the pressed logical key, and this is not needed at all. In this case, I'm hoping not to run the function body, since it's not a trivial amount of work. And as far as I understand, late still always evaluate the value. (I can add a comment to it.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs, the late keyword makes the field lazy, which means the right-hand-side won't run until you access the field. At least that's my understanding.

https://dart.dev/null-safety/understanding-null-safety#lazy-initialization

web_sdk/BUILD.gn Outdated
Comment on lines 291 to 292
inputs =
[ "sdk_rewriter.dart" ] + web_ui_sources + web_locale_keymap_sources
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried without web_locale_keymap_sources? I'm curious because in my case, I didn't add sources from the third_party package, and everything seems to be working fine.

Any ideas @eyebrowsoffire ?

// Character keys, however, can also have more than 1 code unit: en-in
// maps KeyL to L̥/l̥. To resolve this, we check the second code unit.
static bool _eventKeyIsKeyName(String key) {
return key.length > 1 && key.codeUnitAt(0) < 0x7F && key.codeUnitAt(1) < 0x7F;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just say key.length > 2? Are there any names that are only two characters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, such as Fn and F1. And I'm not sure if there are characters of 3 or more code units.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, OK. LGTM then.

@dkwingsmt
Copy link
Contributor Author

@mdebbar I've applied the fix to BUILD.gn as you said. Those changes are indeed not necessary.

Copy link
Contributor

@gspencergoog gspencergoog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

32384589-a60f0e74-c078-11e7-9bc1-e5b5287aea9d

@dkwingsmt dkwingsmt added the autosubmit Merge PR when tree becomes green via auto submit App label Nov 30, 2022
@skia-gold
Copy link

Gold has detected about 1 new digest(s) on patchset 91.
View them at https://flutter-engine-gold.skia.org/cl/github/34625

@auto-submit auto-submit bot merged commit e203623 into flutter:main Nov 30, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Nov 30, 2022
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Nov 30, 2022
…116243)

* 782d3a600 Roll Skia from 6d759de2e5b2 to 24207007711b (3 revisions) (flutter/engine#37968)

* e20362343 [Web, keyboard] Locale layout mapping (flutter/engine#34625)
@dkwingsmt dkwingsmt deleted the web-layout-detection branch December 2, 2022 08:10
mit-mit pushed a commit to mit-mit/flutter that referenced this pull request Dec 6, 2022
…lutter#116243)

* 782d3a600 Roll Skia from 6d759de2e5b2 to 24207007711b (3 revisions) (flutter/engine#37968)

* e20362343 [Web, keyboard] Locale layout mapping (flutter/engine#34625)
shogohida pushed a commit to shogohida/flutter that referenced this pull request Dec 7, 2022
…lutter#116243)

* 782d3a600 Roll Skia from 6d759de2e5b2 to 24207007711b (3 revisions) (flutter/engine#37968)

* e20362343 [Web, keyboard] Locale layout mapping (flutter/engine#34625)
gspencergoog pushed a commit to gspencergoog/flutter that referenced this pull request Jan 19, 2023
…lutter#116243)

* 782d3a600 Roll Skia from 6d759de2e5b2 to 24207007711b (3 revisions) (flutter/engine#37968)

* e20362343 [Web, keyboard] Locale layout mapping (flutter/engine#34625)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
autosubmit Merge PR when tree becomes green via auto submit App platform-web Code specifically for the web engine
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants