blob: 2d8526d8773e31de08af1bc8d4f18ffe82cef39d [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The following segment is not only used in the generating script, but also
// copied to the generated package.
/*@@@ SHARED SEGMENT START @@@*/
/// Used in the final mapping indicating the logical key should be derived from
/// KeyboardEvent.keyCode.
///
/// This value is chosen because it's a printable character within EASCII that
/// will never be mapped to (checked in the marshalling algorithm).
const int kUseKeyCode = 0xFF;
/// Used in the final mapping indicating the event key is 'Dead', the dead key.
final String _kUseDead = String.fromCharCode(0xFE);
/// The KeyboardEvent.key for a dead key.
const String _kEventKeyDead = 'Dead';
/// A map of all goals from the scan codes to their mapped value in US layout.
const Map<String, String> kLayoutGoals = <String, String>{
'KeyA': 'a',
'KeyB': 'b',
'KeyC': 'c',
'KeyD': 'd',
'KeyE': 'e',
'KeyF': 'f',
'KeyG': 'g',
'KeyH': 'h',
'KeyI': 'i',
'KeyJ': 'j',
'KeyK': 'k',
'KeyL': 'l',
'KeyM': 'm',
'KeyN': 'n',
'KeyO': 'o',
'KeyP': 'p',
'KeyQ': 'q',
'KeyR': 'r',
'KeyS': 's',
'KeyT': 't',
'KeyU': 'u',
'KeyV': 'v',
'KeyW': 'w',
'KeyX': 'x',
'KeyY': 'y',
'KeyZ': 'z',
'Digit1': '1',
'Digit2': '2',
'Digit3': '3',
'Digit4': '4',
'Digit5': '5',
'Digit6': '6',
'Digit7': '7',
'Digit8': '8',
'Digit9': '9',
'Digit0': '0',
'Minus': '-',
'Equal': '=',
'BracketLeft': '[',
'BracketRight': ']',
'Backslash': r'\',
'Semicolon': ';',
'Quote': "'",
'Backquote': '`',
'Comma': ',',
'Period': '.',
'Slash': '/',
};
final int _kLowerA = 'a'.codeUnitAt(0);
final int _kUpperA = 'A'.codeUnitAt(0);
final int _kLowerZ = 'z'.codeUnitAt(0);
final int _kUpperZ = 'Z'.codeUnitAt(0);
bool _isAscii(int charCode) {
// 0x20 is the first printable character in ASCII.
return charCode >= 0x20 && charCode <= 0x7F;
}
/// Returns whether the `char` is a single character of a letter or a digit.
bool isLetter(int charCode) {
return (charCode >= _kLowerA && charCode <= _kLowerZ)
|| (charCode >= _kUpperA && charCode <= _kUpperZ);
}
/// A set of rules that can derive a large number of logical keys simply from
/// the event's code and key.
///
/// This greatly reduces the entries needed in the final mapping.
int? heuristicMapper(String code, String key) {
// Digit code: return the digit by event code.
if (code.startsWith('Digit')) {
assert(code.length == 6);
return code.codeUnitAt(5); // The character immediately after 'Digit'
}
final int charCode = key.codeUnitAt(0);
// Non-ascii: return the goal (i.e. US mapping by event code).
if (key.length > 1 || !_isAscii(charCode)) {
return kLayoutGoals[code]?.codeUnitAt(0);
}
// Letter key: return the event key letter.
if (isLetter(charCode)) {
return key.toLowerCase().codeUnitAt(0);
}
return null;
}
// Maps an integer to a printable EASCII character by adding it to this value.
//
// We could've chosen 0x20, the first printable character, for a slightly bigger
// range, but it's prettier this way and sufficient.
final int _kMarshallIntBase = '0'.codeUnitAt(0);
class _StringStream {
_StringStream(this._data) : _offset = 0;
final String _data;
final Map<int, String> _goalToEventCode = Map<int, String>.fromEntries(
kLayoutGoals
.entries
.map((MapEntry<String, String> beforeEntry) =>
MapEntry<int, String>(beforeEntry.value.codeUnitAt(0), beforeEntry.key))
);
int get offest => _offset;
int _offset;
int readIntAsVerbatim() {
final int result = _data.codeUnitAt(_offset);
_offset += 1;
assert(result >= _kMarshallIntBase);
return result - _kMarshallIntBase;
}
int readIntAsChar() {
final int result = _data.codeUnitAt(_offset);
_offset += 1;
return result;
}
String readEventKey() {
final String char = String.fromCharCode(readIntAsChar());
if (char == _kUseDead) {
return _kEventKeyDead;
} else {
return char;
}
}
String readEventCode() {
final int charCode = _data.codeUnitAt(_offset);
_offset += 1;
return _goalToEventCode[charCode]!;
}
}
Map<String, int> _unmarshallCodeMap(_StringStream stream) {
final int entryNum = stream.readIntAsVerbatim();
return Map<String, int>.fromEntries((() sync* {
for (int entryIndex = 0; entryIndex < entryNum; entryIndex += 1) {
yield MapEntry<String, int>(stream.readEventKey(), stream.readIntAsChar());
}
})());
}
/// Decode a key mapping data out of the string.
Map<String, Map<String, int>> unmarshallMappingData(String compressed) {
final _StringStream stream = _StringStream(compressed);
final int eventCodeNum = stream.readIntAsVerbatim();
return Map<String, Map<String, int>>.fromEntries((() sync* {
for (int eventCodeIndex = 0; eventCodeIndex < eventCodeNum; eventCodeIndex += 1) {
yield MapEntry<String, Map<String, int>>(stream.readEventCode(), _unmarshallCodeMap(stream));
}
})());
}
/*@@@ SHARED SEGMENT END @@@*/
/// Whether the given charCode is a ASCII letter.
bool isLetterChar(int charCode) {
return (charCode >= _kLowerA && charCode <= _kLowerZ)
|| (charCode >= _kUpperA && charCode <= _kUpperZ);
}
bool _isPrintableEascii(int charCode) {
return charCode >= 0x20 && charCode <= 0xFF;
}
typedef _ForEachAction<V> = void Function(String key, V value);
void _sortedForEach<V>(Map<String, V> map, _ForEachAction<V> action) {
map
.entries
.toList()
..sort((MapEntry<String, V> a, MapEntry<String, V> b) => a.key.compareTo(b.key))
..forEach((MapEntry<String, V> entry) {
action(entry.key, entry.value);
});
}
// Encode a small integer as a character by its value.
//
// For example, 0x48 is encoded as '0'. This means that values within 0x0 - 0x19
// or greater than 0xFF are forbidden.
void _marshallIntAsChar(StringBuffer builder, int value) {
assert(_isPrintableEascii(value), '$value');
builder.writeCharCode(value);
}
const int _kMarshallIntEnd = 0xFF; // The last printable EASCII.
// Encode a small integer as a character based on a certain printable codepoint.
//
// For example, 0x0 is encoded as '0', and 0x1 is encoded as '1'. This function
// allows smaller values than _marshallIntAsChar.
void _marshallIntAsVerbatim(StringBuffer builder, int value) {
final int result = value + _kMarshallIntBase;
assert(result <= _kMarshallIntEnd);
builder.writeCharCode(result);
}
void _marshallEventCode(StringBuffer builder, String value) {
// Instead of recording the entire eventCode, since the eventCode is mapped
// 1-to-1 to a character in kLayoutGoals, we record the goal instead.
final String char = kLayoutGoals[value]!;
builder.write(char);
}
void _marshallEventKey(StringBuffer builder, String value) {
if (value == _kEventKeyDead) {
builder.write(_kUseDead);
} else {
assert(value.length == 1, value);
assert(value != _kUseDead);
builder.write(value);
}
}
/// Encode a key mapping data into a list of strings.
///
/// The list of strings should be used concatenated, but is returned this way
/// for aesthetic purposes (one entry per line).
///
/// The algorithm aims at encoding the map directly into a printable string
/// (instead of a binary stream converted by base64). Some characters in the
/// string can be multi-byte, which means the decoder should parse the string
/// using substr instead of as a binary stream.
List<String> marshallMappingData(Map<String, Map<String, int>> mappingData) {
final StringBuffer builder = StringBuffer();
_marshallIntAsVerbatim(builder, mappingData.length);
_sortedForEach(mappingData, (String eventCode, Map<String, int> codeMap) {
builder.write('\n');
_marshallEventCode(builder, eventCode);
_marshallIntAsVerbatim(builder, codeMap.length);
_sortedForEach(codeMap, (String eventKey, int logicalKey) {
_marshallEventKey(builder, eventKey);
_marshallIntAsChar(builder, logicalKey);
});
});
return builder.toString().split('\n');
}