mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
Merge pull request #463 from dotCooCoo/fix/keymap-intl-backslash-key
keymap: the ISO key next to left Shift can be bound to shortcuts
This commit is contained in:
8 files changed
+139
-10
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)] {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -224,3 +224,44 @@ 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), "<");
|
||||
|
||||
// UK: the key types `\` and `|`, keys egui has, so it runs the `\` bindings.
|
||||
let uk = Layout::new();
|
||||
let backslash = at("\\", Key::IntlBackslash);
|
||||
let pipe = at("Shift+|", Key::IntlBackslash);
|
||||
assert_eq!(uk.resolve(&backslash), Some(chord("\\")));
|
||||
assert!(uk.matches(&chord("\\"), &backslash, p));
|
||||
assert!(uk.matches(&chord("Shift+\\"), &pipe, p));
|
||||
}
|
||||
@@ -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** (Windows and Linux; macOS reports this key as `` ` ``). 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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user