keymap: the ISO key next to left Shift can be bound to shortcuts

craft_keymap::Key gains IntlBackslash, the key between left Shift and Z
on ISO keyboards (`<` and `>` on Turkish Q). Key::from_egui and to_egui
map it to egui's IntlBackslash, so the shortcut recorder records it and
the dispatcher runs its bindings. Its shortcut text is `IntlBackslash`
(`Cmd+IntlBackslash`, `Shift+IntlBackslash`), read without regard to
case; `<` and `>` still parse as no key.

The key counts as a symbol position: Shift stays in the chord, and once
the layout has typed `<` there, display_for shows `Ctrl+<`.
MAX_CHORD_TEXT goes from 64 to 72 bytes so that the longest canonical
trigger, `Cmd+Ctrl+Alt+Shift+IntlBackslash` pressed twice, reads back.
This commit is contained in:
dotCooCoo committed 2026-10-30 23:00:40 -07:00
1 parent c39e435cc3
commit fc37b20235
8 files changed
+131 -10

No files matched your search

+5 -3
View File
@@ -12,7 +12,7 @@ use crate::layout::Layout;
pub mod foreign;
/// The longest chord or trigger text [`Chord::parse`] and [`Trigger::parse`] accept, in bytes.
pub const MAX_CHORD_TEXT: usize = 64;
pub const MAX_CHORD_TEXT: usize = 72;
/// The platform a keymap is used on. It decides display text and whether `ctrl` folds into `cmd`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@@ -92,7 +92,7 @@ keys! {
Up => "Up", Down => "Down", Left => "Left", Right => "Right",
Minus => "-", Equals => "=", Plus => "+", OpenBracket => "[", CloseBracket => "]",
Backslash => "\\", Semicolon => ";", Quote => "'", Backtick => "`",
Comma => ",", Period => ".", Slash => "/",
Comma => ",", Period => ".", Slash => "/", IntlBackslash => "IntlBackslash",
Colon => ":", Pipe => "|", Questionmark => "?", Exclamationmark => "!",
OpenCurlyBracket => "{", CloseCurlyBracket => "}",
}
@@ -181,7 +181,8 @@ impl Key {
}
/// Punctuation and symbol keys: `-`, `=`, `+`, brackets, `\`, `;`, `'`, `` ` ``, `,`, `.`,
/// `/` and the shifted symbols `:`, `|`, `?`, `!`, `{`, `}`.
/// `/`, the ISO key next to left Shift (`IntlBackslash`) and the shifted symbols `:`, `|`,
/// `?`, `!`, `{`, `}`.
pub const fn is_symbol(self) -> bool {
matches!(
self,
@@ -197,6 +198,7 @@ impl Key {
| Key::Comma
| Key::Period
| Key::Slash
| Key::IntlBackslash
| Key::Colon
| Key::Pipe
| Key::Questionmark
+3 -3
View File
@@ -35,8 +35,8 @@ impl From<OperatingSystem> for Platform {
macro_rules! egui_keys {
($($ours:ident = $theirs:ident),* ; $($same:ident),* $(,)?) => {
impl Key {
/// The key for an egui key. `None` for modifier keys, Copy, Cut, Paste, BrowserBack
/// and IntlBackslash.
/// The key for an egui key. `None` for modifier keys, Copy, Cut, Paste and
/// BrowserBack.
pub fn from_egui(key: EguiKey) -> Option<Key> {
match key {
$(EguiKey::$theirs => Some(Key::$ours),)*
@@ -63,7 +63,7 @@ egui_keys! {
F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32, F33, F34, F35,
Escape, Tab, Backspace, Enter, Space, Insert, Delete, Home, End, PageUp, PageDown,
Minus, Equals, Plus, OpenBracket, CloseBracket, Backslash, Semicolon, Quote, Backtick,
Comma, Period, Slash, Colon, Pipe, Questionmark, Exclamationmark, OpenCurlyBracket, CloseCurlyBracket,
Comma, Period, Slash, IntlBackslash, Colon, Pipe, Questionmark, Exclamationmark, OpenCurlyBracket, CloseCurlyBracket,
}
impl Mods {
+8 -3
View File
@@ -15,7 +15,9 @@
//! 3. **Punctuation:** the character typed, without Shift when the layout needs Shift for it
//! (German `/` is ⇧7, so ⇧⌘7 is ⌘/; US `:` is ⇧;, so ⇧; is `:`). A Shift binding on a US
//! base key also matches the same press (`Shift+;` matches `:`), and `=` and `+` stand for
//! each other so zoom keys work with and without Shift.
//! each other so zoom keys work with and without Shift. egui has no key for the characters the
//! ISO key next to left Shift types (`<` and `>` on Turkish Q) and reports both as that key,
//! `IntlBackslash`, so Shift stays in the chord: ⇧ on that key is `Shift+IntlBackslash`.
//! 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. **No position fallback for unnamed characters on moved keys:** egui reports a character
@@ -293,8 +295,11 @@ impl Layout {
if Key::from_char(typed).is_some() {
return None;
}
let own = key.name().chars().next()?;
if self.chars.values().any(|&c| c == own) {
// `IntlBackslash` types no character of its own, so only a one-character name is checked.
let mut name = key.name().chars();
if let (Some(own), None) = (name.next(), name.next())
&& self.chars.values().any(|&c| c == own)
{
return None;
}
let mut upper = typed.to_uppercase();
+18
View File
@@ -99,4 +99,22 @@ mod tests {
assert_eq!(chords, ["Cmd+E", "Tab", "Cmd+C", "Cmd+V"]);
assert_eq!(left, 1, "only the pointer event stays");
}
#[test]
fn records_the_iso_key_next_to_left_shift() {
// Turkish Q: the key types `<`, and `>` with Shift.
let release = |modifiers| Event::Key { key: EguiKey::IntlBackslash, physical_key: None, pressed: false, repeat: false, modifiers };
let events = vec![
press(EguiKey::IntlBackslash, Modifiers::NONE),
Event::Text("<".into()),
release(Modifiers::NONE),
press(EguiKey::IntlBackslash, Modifiers::SHIFT),
Event::Text(">".into()),
release(Modifiers::SHIFT),
press(EguiKey::IntlBackslash, Modifiers::MAC_CMD),
];
let (chords, left) = run(events);
assert_eq!(chords, ["IntlBackslash", "Shift+IntlBackslash", "Cmd+IntlBackslash"]);
assert_eq!(left, 0);
}
}
+23
View File
@@ -34,6 +34,29 @@ fn plus_and_minus_keys_parse_from_their_glyphs() {
assert_eq!(Key::parse("−"), Some(Key::Minus), "U+2212");
}
#[test]
fn the_iso_key_next_to_left_shift_reads_and_writes_as_intl_backslash() {
assert_eq!(Key::parse("IntlBackslash"), Some(Key::IntlBackslash));
assert_eq!(Key::parse("intlbackslash"), Some(Key::IntlBackslash));
for text in ["IntlBackslash", "Cmd+IntlBackslash", "Shift+IntlBackslash"] {
let c = chord(text);
assert_eq!(c.to_string(), text);
assert_eq!(Trigger::parse(text).map(|t| t.to_string()).as_deref(), Ok(text));
let json = serde_json::to_string(&c).unwrap();
assert_eq!(json, format!("\"{text}\""));
assert_eq!(serde_json::from_str::<Chord>(&json).unwrap(), c);
}
assert_eq!(chord("Cmd+IntlBackslash").display(Platform::Windows), "Ctrl+IntlBackslash");
assert_eq!(chord("Cmd+IntlBackslash").display(Platform::Mac), "⌘IntlBackslash");
assert!(Key::IntlBackslash.is_symbol());
// It is not the usual backslash key, and `<` and `>` have no key of their own.
assert_eq!(Key::parse("\\"), Some(Key::Backslash));
assert_ne!(chord("Shift+IntlBackslash"), chord("Shift+\\"));
assert_eq!(Key::parse("<"), None);
assert_eq!(Key::from_char('<'), None);
assert_eq!(Key::from_char('>'), None);
}
#[test]
fn arrow_glyphs_parse_as_arrow_keys() {
for (glyph, key) in [("←", Key::Left), ("→", Key::Right), ("↑", Key::Up), ("↓", Key::Down)] {
+37 -1
View File
@@ -22,9 +22,11 @@ fn every_key_converts_both_ways() {
for &key in Key::ALL {
assert_eq!(Key::from_egui(key.to_egui()), Some(key), "{key}");
}
for k in [EguiKey::Copy, EguiKey::Cut, EguiKey::Paste, EguiKey::BrowserBack, EguiKey::ShiftLeft, EguiKey::IntlBackslash] {
for k in [EguiKey::Copy, EguiKey::Cut, EguiKey::Paste, EguiKey::BrowserBack, EguiKey::ShiftLeft] {
assert_eq!(Key::from_egui(k), None);
}
assert_eq!(Key::from_egui(EguiKey::IntlBackslash), Some(Key::IntlBackslash));
assert_eq!(Key::IntlBackslash.to_egui(), EguiKey::IntlBackslash);
}
#[test]
@@ -508,6 +510,40 @@ fn the_dispatcher_reads_presses_by_the_layout() {
assert_eq!(ran, ["edit.duplicate"]);
}
#[test]
fn the_iso_key_next_to_left_shift_runs_its_bindings() {
let app = |id: &str, key: &str| Bindable::new(id, id, Scope::app()).with_default(Binding::parse(key).unwrap());
let km = Keymap::new(
vec![
app("iso", "IntlBackslash"),
app("iso.shift", "Shift+IntlBackslash"),
app("iso.ctrl", "Cmd+IntlBackslash"),
app("backslash", "\\"),
app("pipe", "Shift+\\"),
app("comma.shift", "Shift+,"),
],
Scopes::new(),
Platform::Windows,
);
// Turkish Q: the key types `<`, and `>` with Shift. egui has no key for either character and
// reports the press as the key itself.
let at =
|modifiers| Event::Key { key: EguiKey::IntlBackslash, physical_key: Some(EguiKey::IntlBackslash), pressed: true, repeat: false, modifiers };
let shift = Modifiers::SHIFT;
let mut d = Dispatcher::new();
let (ran, left) = run(&mut d, &km, &Active::NONE, frame(1.0, Modifiers::NONE, vec![at(Modifiers::NONE), Event::Text("<".into())]));
assert_eq!(ran, ["iso"]);
assert!(left.is_empty(), "{left:?}");
let (ran, left) = run(&mut d, &km, &Active::NONE, frame(2.0, shift, vec![at(shift), Event::Text(">".into())]));
assert_eq!(ran, ["iso.shift"]);
assert!(left.is_empty(), "{left:?}");
let (ran, _) = run(&mut d, &km, &Active::NONE, frame(3.0, CTRL, vec![at(CTRL)]));
assert_eq!(ran, ["iso.ctrl"]);
// The press without Shift taught the layout the key's character.
assert_eq!(chord("Cmd+IntlBackslash").display_for(Platform::Windows, d.layout()), "Ctrl+<");
assert_eq!(chord("Shift+IntlBackslash").display_for(Platform::Windows, d.layout()), "Shift+<");
}
#[test]
fn letters_follow_the_latin_layout_from_egui_winit_to_the_dispatcher() {
let app = |id: &str, key: &str| Bindable::new(id, id, Scope::app()).with_default(Binding::parse(key).unwrap());
+33
View File
@@ -224,3 +224,36 @@ fn labels_show_the_layouts_own_punctuation() {
assert_eq!(craft_keymap::Trigger::double(chord("[")).display_for(Platform::Linux, &russian), "Х Х");
assert_eq!(label(&Layout::new(), "Cmd+[", Platform::Linux), "Ctrl+[");
}
#[test]
fn the_iso_key_next_to_left_shift_keeps_shift_and_shows_the_layouts_character() {
let p = Platform::Windows;
// Turkish Q: the key types `<`, and `>` with Shift, which egui reports as the key itself.
let plain = at("IntlBackslash", Key::IntlBackslash);
let shifted = at("Shift+IntlBackslash", Key::IntlBackslash);
let mut turkish = Layout::new();
assert_eq!(turkish.resolve(&plain), Some(chord("IntlBackslash")));
assert_eq!(turkish.resolve(&shifted), Some(chord("Shift+IntlBackslash")));
assert_eq!(texts(&turkish.candidates(&shifted)), ["Shift+IntlBackslash"]);
assert_eq!(turkish.resolve(&at("Cmd+IntlBackslash", Key::IntlBackslash)), Some(chord("Cmd+IntlBackslash")));
assert!(turkish.matches(&chord("IntlBackslash"), &plain, p));
assert!(turkish.matches(&chord("Shift+IntlBackslash"), &shifted, p));
for other in ["Shift+IntlBackslash", "\\", ",", "Shift+,"] {
assert!(!turkish.matches(&chord(other), &plain, p), "{other}");
}
for other in ["IntlBackslash", "\\", "Shift+\\", "|", ",", "Shift+,"] {
assert!(!turkish.matches(&chord(other), &shifted, p), "Shift: {other}");
}
let label = |layout: &Layout, text: &str, p| chord(text).display_for(p, layout);
assert_eq!(label(&turkish, "Cmd+IntlBackslash", p), "Ctrl+IntlBackslash", "nothing typed yet");
turkish.observe(Key::IntlBackslash, Mods::SHIFT, ">");
assert_eq!(turkish.char_at(Key::IntlBackslash), None, "a Shift press teaches nothing");
turkish.observe(Key::IntlBackslash, Mods::NONE, "<");
assert_eq!(label(&turkish, "Cmd+IntlBackslash", p), "Ctrl+<");
assert_eq!(label(&turkish, "Shift+IntlBackslash", p), "Shift+<");
assert_eq!(label(&turkish, "Cmd+IntlBackslash", Platform::Mac), "⌘<");
// The key's name is no character, so a position that types `I` does not hide the label.
let typed_i = Layout::new().with_char(Key::IntlBackslash, '<').with_char(Key::Quote, 'I');
assert_eq!(label(&typed_i, "IntlBackslash", p), "<");
}
+4
View File
@@ -117,6 +117,10 @@ shortcut as listed. On macOS read Ctrl as Cmd.
Q position, which types `'`, does not run Ctrl+Q.
- [ ] **QWERTZ** (German). Ctrl+Z undoes and Ctrl+Y redoes, on the keys labelled Z and Y. Ctrl+⇧7
runs Ctrl+/. The menu shows Ctrl+[ as Ctrl+Ü.
- [ ] **Turkish Q.** The Keyboard Shortcuts dialog records the key next to left Shift, which
types `<`, as `IntlBackslash`, and with Shift held as `Shift+IntlBackslash`. Each runs its
command, and neither runs a `\` or Shift+, binding. Once `<` has been typed, the menu shows
Ctrl+IntlBackslash as Ctrl+<.
- [ ] **Dvorak – QWERTY ⌘** (macOS). Typing gives Dvorak letters. With Cmd held, the keys follow
QWERTY: Cmd on the QWERTY C position copies, and the menus show ⌘C.