From d0acf36588bf191f8b7d0e1643fad509ed71ebd0 Mon Sep 17 00:00:00 2001 From: aiongg Date: Wed, 21 Oct 2026 05:47:51 +0000 Subject: [PATCH] keymap: Shift presses egui reports as their position run nothing on moved layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dvorak's ⇧, types <, which egui has no key for, so egui reports the press as W and Ctrl+Shift+, ran Ctrl+Shift+W. Once typing shows the layout's letters are not on QWERTY positions, such a press resolves to nothing unless the position is known to type its own letter. --- crates/keymap/README.md | 5 ++++- crates/keymap/src/layout.rs | 32 ++++++++++++++++++++++++++++++-- crates/keymap/tests/layout.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/crates/keymap/README.md b/crates/keymap/README.md index f4719461..3d19cd31 100644 --- a/crates/keymap/README.md +++ b/crates/keymap/README.md @@ -93,7 +93,10 @@ macOS it folds into `cmd` (`Chord::effective`). 3. Punctuation: the character typed, without Shift when the layout needs Shift for it: German `/` is ⇧7, so ⇧⌘7 runs ⌘/. 4. Whether a layout is Latin is decided for the layout, not per key. -5. Labels show letters and digits by name, and a punctuation key as the character the layout +5. With Shift held on a Latin layout whose letters are known to be moved (Dvorak), a press that + egui reports as the letter at its own position, because the character typed has no key + (Dvorak's ⇧, types `<`), runs nothing: Ctrl+Shift+, never runs Ctrl+Shift+W. +6. Labels show letters and digits by name, and a punctuation key as the character the layout types at its position when that character has no key of its own (`Ctrl+Ü` for Ctrl+[ on German). diff --git a/crates/keymap/src/layout.rs b/crates/keymap/src/layout.rs index 3beacce3..0605db17 100644 --- a/crates/keymap/src/layout.rs +++ b/crates/keymap/src/layout.rs @@ -16,7 +16,12 @@ //! each other so zoom keys work with and without Shift. //! 4. **Whether a layout is Latin** is decided for the whole layout ([`Layout::is_latin`]), never //! per key: Dvorak's `'` key on the Q position stays `'`. -//! 5. **Labels** show letters and digits by name, and a punctuation key as the character the +//! 5. **No position fallback for unnamed characters on moved layouts:** egui reports a character +//! that has no key (Dvorak's ⇧, types `<`) as the key's position, W. With Shift held on a +//! Latin layout whose letters are known to sit elsewhere than on QWERTY, such a press at a +//! letter position resolves to nothing unless that position is known to type its own letter, +//! so Ctrl+Shift+, never runs Ctrl+Shift+W. +//! 6. **Labels** show letters and digits by name, and a punctuation key as the character the //! layout types at its position when that character has no key of its own: Ctrl+[ is `Ctrl+Ü` //! on German and `Ctrl+Х` on Russian. //! @@ -92,6 +97,8 @@ pub struct Layout { seen_latin: Option, /// The character typed without modifiers at each punctuation position. chars: BTreeMap, + /// The character, lower case, typed without Cmd, Ctrl or Alt at each letter position. + letters: BTreeMap, } /// US base keys and the key egui reports for them with Shift held. @@ -175,6 +182,9 @@ impl Layout { if c.is_alphabetic() { self.seen_latin = Some(is_latin_letter(c)); } + if !c.is_control() { + self.letters.insert(physical, c.to_lowercase().next().unwrap_or(c)); + } } else if !mods.shift && !c.is_control() { self.set_char(physical, c); } @@ -192,11 +202,29 @@ impl Layout { { return Some(Chord::new(held, position)); } + if held.shift + && let Some(position) = input.physical.filter(|k| k.is_letter()) + && input.typed.is_none_or(|typed| typed == position) + && !self.types_own_letter(position) + { + return None; + } let key = input.typed.or(input.physical)?; let drop_shift = held.shift && key.is_symbol() && input.needs_shift(key); Some(Chord::new(if drop_shift { without_shift(held) } else { held }, key)) } + /// Rule 5: whether a press reported as the letter at its own position can be that letter. + /// Yes when the position is known to type it, or nothing is known to sit elsewhere than on + /// QWERTY; otherwise the press likely typed a character egui has no key for. + fn types_own_letter(&self, position: Key) -> bool { + let own = |key: Key, c: char| Key::from_char(c) == Some(key); + match self.letters.get(&position) { + Some(&c) => own(position, c), + None => self.letters.iter().all(|(&key, &c)| own(key, c)), + } + } + /// The chords a press can run, most specific first: [`resolve`](Self::resolve)'s chord, the /// same press written as Shift with a US base key (`Shift+;` for `:`), `=` for `+` and `+` /// for Shift+`=`, and on the digit row the position's digit. @@ -235,7 +263,7 @@ impl Layout { self.candidates(input).iter().any(|c| c.effective(platform) == want) } - /// Rule 5: the layout's character for the punctuation key `key`, when pressing that position + /// Rule 6: the layout's character for the punctuation key `key`, when pressing that position /// means `key` and the layout types `key`'s own character nowhere else that is known. pub(crate) fn key_label(&self, key: Key) -> Option { let typed = self.char_at(key)?; diff --git a/crates/keymap/tests/layout.rs b/crates/keymap/tests/layout.rs index 31f344f3..e143a353 100644 --- a/crates/keymap/tests/layout.rs +++ b/crates/keymap/tests/layout.rs @@ -136,6 +136,34 @@ fn typing_decides_whether_the_layout_is_latin() { assert!(reported.is_latin()); } +#[test] +fn dvorak_shift_punctuation_never_runs_its_position() { + // Dvorak's , key sits on W and types < with Shift, which egui has no key for, so egui + // reports the press as W. + let ctrl_shift_lt = at("Cmd+Shift+W", Key::W); + let mut dvorak = Layout::new(); + assert_eq!(dvorak.resolve(&ctrl_shift_lt), Some(chord("Cmd+Shift+W")), "nothing known yet"); + + dvorak.observe(Key::S, Mods::NONE, "o"); + assert_eq!(dvorak.resolve(&ctrl_shift_lt), None, "letters are known to be moved"); + assert!(!dvorak.matches(&chord("Ctrl+Shift+W"), &ctrl_shift_lt, Platform::Windows)); + dvorak.observe(Key::W, Mods::NONE, ","); + assert_eq!(dvorak.resolve(&ctrl_shift_lt), None); + // Without Shift, and for letters typed where they sit (Dvorak's A) or elsewhere, presses + // resolve as before. + assert_eq!(dvorak.resolve(&at("Cmd+W", Key::W)), Some(chord("Cmd+W"))); + dvorak.observe(Key::A, Mods::NONE, "a"); + assert_eq!(dvorak.resolve(&at("Cmd+Shift+A", Key::A)), Some(chord("Cmd+Shift+A"))); + assert_eq!(dvorak.resolve(&at("Cmd+Shift+O", Key::S)), Some(chord("Cmd+Shift+O"))); + + // QWERTY: a position typing its own letter keeps the position fallback. + let mut qwerty = Layout::new(); + qwerty.observe(Key::S, Mods::NONE, "s"); + qwerty.observe(Key::W, Mods::SHIFT, "W"); + assert_eq!(qwerty.resolve(&ctrl_shift_lt), Some(chord("Cmd+Shift+W"))); + assert_eq!(qwerty.resolve(&at("Cmd+Shift+E", Key::E)), Some(chord("Cmd+Shift+E"))); +} + #[test] fn labels_show_the_layouts_own_punctuation() { let mut german = Layout::new().with_char(Key::OpenBracket, 'ü').with_char(Key::Slash, '-').with_char(Key::CloseBracket, '+');