keymap: a key whose Latin layout character is no letter runs no letter binding

The suite's egui reports, per key pressed, the character it types on the
Latin layout shortcuts follow (InputState::latin_layout_char). A letter
makes the press that letter; any other character means the key has no
Latin letter, so egui's position fallback no longer turns it into one: on
Dvorak + Russian the key that types й is Dvorak's ', not Ctrl+Q. Without a
report (crates.io egui) the key's position still decides.
This commit is contained in:
aiongg committed 2026-10-21 18:05:18 +00:00
1 parent cec769a889
commit 315af428c7
7 files changed
+133 -27

No files matched your search

Generated
+25
View File
@@ -8,6 +8,8 @@ version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
dependencies = [
"enumn",
"serde",
"uuid",
]
@@ -35,6 +37,7 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"serde",
"version_check",
"zerocopy",
]
@@ -908,6 +911,7 @@ checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
dependencies = [
"bytemuck",
"emath",
"serde",
]
[[package]]
@@ -957,6 +961,7 @@ dependencies = [
"log",
"nohash-hasher",
"profiling",
"serde",
"smallvec",
"unicode-segmentation",
"web-sys",
@@ -1048,6 +1053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
dependencies = [
"bytemuck",
"serde",
]
[[package]]
@@ -1084,6 +1090,17 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "enumn"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "epaint"
version = "0.36.2"
@@ -1102,6 +1119,7 @@ dependencies = [
"parking_lot",
"profiling",
"self_cell",
"serde",
"skrifa 0.44.0",
"smallvec",
"unicode-general-category",
@@ -1265,6 +1283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
dependencies = [
"bytemuck",
"serde",
]
[[package]]
@@ -4013,6 +4032,9 @@ name = "smallvec"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
dependencies = [
"serde",
]
[[package]]
name = "smithay-client-toolkit"
@@ -4753,6 +4775,9 @@ name = "uuid"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
dependencies = [
"serde_core",
]
[[package]]
name = "vello_common"
+2
View File
@@ -16,6 +16,8 @@ serde_json = "1"
egui = { workspace = true, optional = true }
[dev-dependencies]
# For setting `RawInput` fields only the suite's egui fork has.
egui = { workspace = true, features = ["serde"] }
craft-settings = { workspace = true }
tempfile = "3"
+5 -4
View File
@@ -86,10 +86,11 @@ macOS it folds into `cmd` (`Chord::effective`).
(`Chord::display_for`):
1. Letters A–Z: the layout's Latin letter for the key. On a layout that is not Latin (Russian,
Arabic, Hebrew, Greek, Thai), the letter the toolkit reports for shortcuts, else the key's
position: the suite's egui-winit reports the Latin layout's letter, so Ctrl+С on Dvorak +
Russian is Dvorak's Ctrl+J. On a Latin layout (Dvorak, AZERTY), the letter typed. Arabic
Ctrl+Shift+D is Ctrl+Shift+D, not the `]` it types.
Arabic, Hebrew, Greek, Thai), the suite's egui reports the Latin layout's character for each
key: Ctrl+С on Dvorak + Russian is Dvorak's Ctrl+J, and Ctrl+Й, `'` on Dvorak, runs no letter
binding. Without that report, the letter the toolkit reports for shortcuts, else the key's
position. On a Latin layout (Dvorak, AZERTY), the letter typed. Arabic Ctrl+Shift+D is
Ctrl+Shift+D, not the `]` it types.
2. Digits: the digit typed, else the key's position (AZERTY's `&é"` row). A digit-row key that
types punctuation also matches its digit when the punctuation has no binding.
3. Punctuation: the character typed, without Shift when the layout needs Shift for it: German
+30 -7
View File
@@ -93,9 +93,10 @@ impl Chord {
impl KeyInput {
/// What an egui key press reports: the logical key as typed and the physical key. On a layout
/// that is not Latin, the suite's egui-winit reports the Latin layout's letter as the logical
/// key (Linux, Windows, and macOS with ⌘ held), and crates.io egui the physical key, so [`Layout::resolve`] takes a
/// letter there before the position. egui reports no Shift level, so [`Layout`] infers it.
/// `None` for events other than key presses and releases.
/// key (Linux, Windows, and macOS with ⌘ held), and crates.io egui the physical key, so
/// [`Layout::resolve`] takes a letter there before the position. The Latin layout's character
/// comes from the frame's input ([`Dispatcher::dispatch`] fills it in). egui reports no Shift
/// level, so [`Layout`] infers it. `None` for events other than key presses and releases.
pub fn from_egui(event: &Event, platform: Platform) -> Option<KeyInput> {
let Event::Key { key, physical_key, modifiers, .. } = event else { return None };
let mut input = KeyInput::new(Mods::from_egui(modifiers, platform));
@@ -285,12 +286,12 @@ impl Dispatcher {
let mut drop = vec![false; events.len()];
// The web build sends both a clipboard key press and the clipboard event it causes. The
// press decides: its event goes with it when it runs a command and stays otherwise.
let causes = clipboard_event_follows(&events, &self.layout, platform);
let causes = clipboard_event_follows(&events, &self.layout, platform, input);
let mut caused_taken: Vec<Option<bool>> = vec![None; events.len()];
for (i, event) in events.into_iter().enumerate() {
match &event {
Event::Key { pressed: true, repeat, .. } if !*repeat || self.repeat => {
let input = KeyInput::from_egui(&event, platform).unwrap_or_default();
let input = key_input(&event, platform, input);
let held = input.mods;
if let Some(chord) = self.layout.resolve(&input) {
let plain_clip = is_plain_clipboard(chord.key, chord.mods);
@@ -423,14 +424,36 @@ trait KeyboardLayoutIsLatin {
impl KeyboardLayoutIsLatin for InputState {}
fn clipboard_event_follows(events: &[Event], layout: &Layout, platform: Platform) -> Vec<Option<(usize, Option<String>)>> {
/// The suite's egui fork has an inherent `InputState::latin_layout_char`, the character a key
/// pressed this frame types on the Latin layout shortcuts follow; crates.io egui reports none.
/// Reached as [`KeyboardLayoutIsLatin`] is.
#[allow(dead_code, reason = "unused when egui has the inherent method")]
trait LatinLayoutChar {
fn latin_layout_char(&self, _physical_key: EguiKey) -> Option<char> {
None
}
}
impl LatinLayoutChar for InputState {}
/// A press as [`KeyInput::from_egui`] reads it, with the Latin layout's character `input` reports
/// for its key.
fn key_input(event: &Event, platform: Platform, input: &InputState) -> KeyInput {
let mut key = KeyInput::from_egui(event, platform).unwrap_or_default();
if let Event::Key { physical_key: Some(physical), .. } = event {
key.latin = input.latin_layout_char(*physical);
}
key
}
fn clipboard_event_follows(events: &[Event], layout: &Layout, platform: Platform, input: &InputState) -> Vec<Option<(usize, Option<String>)>> {
let mut out = vec![None; events.len()];
let mut ahead: [Option<usize>; 3] = [None; 3];
for (i, event) in events.iter().enumerate().rev() {
if let Some(seen) = event_clip(event).and_then(|c| ahead.get_mut(c as usize)) {
*seen = Some(i);
} else if let Event::Key { pressed: true, .. } = event {
let chord = KeyInput::from_egui(event, platform).and_then(|input| layout.resolve(&input));
let chord = layout.resolve(&key_input(event, platform, input));
let clip = chord.and_then(|c| clipboard_kind(c.key, c.mods));
if let Some(j) = clip.and_then(|c| ahead.get_mut(c as usize)).and_then(Option::take) {
let text = if let Some(Event::Paste(text)) = events.get(j) { Some(text.clone()) } else { None };
+24 -14
View File
@@ -3,11 +3,12 @@
//!
//! One rule decides matching ([`Layout::candidates`]), recording ([`Layout::resolve`]) and labels
//! ([`Chord::display_for`]):
//! 1. **Letters A–Z:** the layout's Latin letter for the key, [`KeyInput::latin`] when the
//! platform reports it. Without it, on a layout that is not Latin, the letter the toolkit
//! reports as typed, else the key's position (the Russian `с` key is C, the Hebrew `/` key is
//! Q). The suite's egui-winit reports the Latin layout's letter there, so on Dvorak + Russian
//! the `с` key is Dvorak's J. On a Latin layout (Dvorak, AZERTY), the letter typed.
//! 1. **Letters A–Z:** the layout's Latin letter for the key, from [`KeyInput::latin`] when the
//! platform reports the Latin layout's character: on Dvorak + Russian the `с` key is Dvorak's
//! J, and the `й` key, `'` on Dvorak, runs no letter binding. Without a report, on a layout
//! that is not Latin, the letter the toolkit reports as typed, else the key's position (the
//! Russian `с` key is C, the Hebrew `/` key is Q); on a Latin layout (Dvorak, AZERTY), the
//! letter typed.
//! 2. **Digits:** the digit typed, else the key's position (AZERTY's `&é"` row types no digit
//! without Shift). A digit-row press that types punctuation also matches its position's digit,
//! after the punctuation: AZERTY's `-` key runs Ctrl+- and, when that has no binding, Ctrl+6.
@@ -46,10 +47,12 @@ pub struct KeyInput {
/// The key at the same position on a US QWERTY keyboard (winit's physical key, the web's
/// `code`).
pub physical: Option<Key>,
/// The layout's Latin letter for this key, when the platform reports one: the virtual-key
/// code on Windows, the layout's ⌘ table on macOS, the first Latin layout group on Linux, the
/// key code on the web when the layout is not Latin. Keys other than letters are ignored.
pub latin: Option<Key>,
/// The character this key types on the layout the platform takes shortcut letters from, when
/// it reports one: the layout's ⌘ table on macOS, the first Latin layout group on Linux. A
/// letter makes the press that letter; any other character means the key has no Latin letter,
/// so the press runs no letter binding (Dvorak's `'` on the key Russian types `й` on is no
/// Ctrl+Q).
pub latin: Option<char>,
/// Whether the layout needs Shift to type `typed`, when the platform reports it. Otherwise
/// Shift counts as needed when `typed` differs from `physical`, and without `physical` when
/// `typed` is a shifted symbol on a US layout (`:`, `?`, `+`). Only punctuation drops Shift.
@@ -71,8 +74,8 @@ impl KeyInput {
self
}
pub const fn with_latin(mut self, letter: Key) -> KeyInput {
self.latin = Some(letter);
pub const fn with_latin(mut self, c: char) -> KeyInput {
self.latin = Some(c);
self
}
@@ -196,12 +199,14 @@ impl Layout {
/// `None` when the press names no key.
pub fn resolve(&self, input: &KeyInput) -> Option<Chord> {
let held = input.mods;
if let Some(letter) = input.latin.filter(|k| k.is_letter()) {
let latin = input.latin.map(|c| Key::from_char(c).filter(|k| k.is_letter()));
if let Some(Some(letter)) = latin {
return Some(Chord::new(held, letter));
}
// A letter typed on a layout that is not Latin is the toolkit's letter for shortcuts, or
// egui standing in the position for a letter it has no key for.
if !self.is_latin()
if latin.is_none()
&& !self.is_latin()
&& let Some(letter) = input.typed.filter(|k| k.is_letter()).or(input.physical).filter(|k| k.is_letter())
{
return Some(Chord::new(held, letter));
@@ -213,7 +218,12 @@ impl Layout {
{
return None;
}
let key = input.typed.or(input.physical)?;
let mut key = input.typed.or(input.physical)?;
// The Latin layout has no letter here, so a letter egui reports is its position: the
// press is the Latin layout's key, or nothing.
if let (Some(c), true) = (input.latin, key.is_letter()) {
key = Key::from_char(c)?;
}
let drop_shift = held.shift && key.is_symbol() && input.needs_shift(key);
Some(Chord::new(if drop_shift { without_shift(held) } else { held }, key))
}
+34
View File
@@ -537,3 +537,37 @@ fn letters_follow_the_latin_layout_from_egui_winit_to_the_dispatcher() {
assert_eq!(ran, [expected], "{name}, reported");
}
}
/// `input` with the Latin layout characters the suite's egui-winit reports per physical key, set
/// through serde because crates.io egui has no such field; `None` there.
fn with_latin_layout_chars(mut input: InputState, chars: &[(EguiKey, char)]) -> Option<InputState> {
let mut raw = serde_json::to_value(&input.raw).unwrap();
raw.get("latin_layout_chars")?;
raw["latin_layout_chars"] = serde_json::to_value(chars).unwrap();
input.raw = serde_json::from_value(raw).unwrap();
Some(input)
}
#[test]
fn a_key_without_a_latin_letter_runs_no_letter_binding() {
let app = |id: &str, key: &str| Bindable::new(id, id, Scope::app()).with_default(Binding::parse(key).unwrap());
let km = Keymap::new(vec![app("quit", "Cmd+Q"), app("quote", "Cmd+'"), app("j", "Cmd+J")], Scopes::new(), Platform::Linux);
let at = |key, physical| Event::Key { key, physical_key: Some(physical), pressed: true, repeat: false, modifiers: CTRL };
// Dvorak + Russian. The QWERTY Q key types й, and Dvorak ' there: egui-winit reports the key
// as ', and egui without a key for the character as its position, Q. The QWERTY C key types
// с, and Dvorak j.
let latin = [(EguiKey::Q, '\''), (EguiKey::C, 'j')];
let cases = [(at(EguiKey::Quote, EguiKey::Q), "quote"), (at(EguiKey::Q, EguiKey::Q), "quote"), (at(EguiKey::J, EguiKey::C), "j")];
for (press, expected) in cases {
let mut d = Dispatcher::new().with_layout(Layout::new().with_latin(false));
let Some(input) = with_latin_layout_chars(frame(1.0, CTRL, vec![press.clone()]), &latin) else {
// crates.io egui reports no Latin layout, so the position decides: Ctrl+Q.
let (ran, _) = run(&mut d, &km, &Active::NONE, frame(1.0, CTRL, vec![at(EguiKey::Q, EguiKey::Q)]));
assert_eq!(ran, ["quit"]);
return;
};
let (ran, _) = run(&mut d, &km, &Active::NONE, input);
assert_eq!(ran, [expected], "{press:?}");
}
}
+13 -2
View File
@@ -35,6 +35,14 @@ fn each_layout_resolves_presses_by_the_rule() {
// Greek: Q types ;.
("Greek ⌃Q", &other, at("Cmd+;", Key::Q), "Cmd+Q"),
("Greek ⌃Z", &other, at("Cmd+Z", Key::Z), "Cmd+Z"),
// Dvorak + Russian, as the suite's egui-winit reports them: the Latin layout's character
// for each key. Dvorak types ' on the Q position, which egui reports as Q or as '.
("Dvorak + Russian ⌃С", &other, at("Cmd+J", Key::C).with_latin('j'), "Cmd+J"),
("Dvorak + Russian ⌃Й", &other, at("Cmd+Q", Key::Q).with_latin('\''), "Cmd+'"),
("Dvorak + Russian ⌃Й as '", &other, at("Cmd+'", Key::Q).with_latin('\''), "Cmd+'"),
("Dvorak + Russian / key types .", &other, at("Cmd+.", Key::Slash).with_latin('z'), "Cmd+Z"),
// Dvorak + Hebrew: the Q position types / and has no Dvorak letter.
("Dvorak + Hebrew ⌃Q", &other, at("Cmd+/", Key::Q).with_latin('\''), "Cmd+/"),
// Thai Kedmanee: 2 types /, - types ข, D types ก.
("Thai 2 key types /", &other, at("Cmd+/", Key::Num2), "Cmd+/"),
("Thai - key types ข", &other, at("Cmd+-", Key::Minus), "Cmd+-"),
@@ -64,7 +72,7 @@ fn each_layout_resolves_presses_by_the_rule() {
("US ⇧- types _, which has no key", &latin, at("Shift+-", Key::Minus), "Shift+-"),
// macOS "Dvorak – QWERTY ⌘": with ⌘ held the layout's ⌘ table is QWERTY, while the
// logical key egui reports is Dvorak's K.
("Dvorak – QWERTY ⌘ ⌘V", &latin, at("Cmd+K", Key::V).with_latin(Key::V), "Cmd+V"),
("Dvorak – QWERTY ⌘ ⌘V", &latin, at("Cmd+K", Key::V).with_latin('v'), "Cmd+V"),
// A Shift level the platform reports wins over the inference.
("reported Shift level", &latin, at("Cmd+Shift+/", Key::Num7).with_shift_needed(false), "Cmd+Shift+/"),
];
@@ -81,7 +89,10 @@ fn reported_cases_match_only_the_chord_the_press_means() {
assert!(arabic.matches(&chord("Ctrl+Shift+D"), &ctrl_shift_d, p));
assert!(!arabic.matches(&chord("Ctrl+Shift+]"), &ctrl_shift_d, p), "Arabic Ctrl+Shift+D must not run Ctrl+Shift+]");
let qwerty_cmd = at("Cmd+K", Key::V).with_latin(Key::V);
let dvorak_russian_q = at("Cmd+Q", Key::Q).with_latin('\'');
assert!(!Layout::new().with_latin(false).matches(&chord("Ctrl+Q"), &dvorak_russian_q, p), "Dvorak + Russian й must not quit");
let qwerty_cmd = at("Cmd+K", Key::V).with_latin('v');
assert!(Layout::new().matches(&chord("Cmd+V"), &qwerty_cmd, Platform::Mac));
assert!(!Layout::new().matches(&chord("Cmd+K"), &qwerty_cmd, Platform::Mac), "⌘V must not open the command palette");