Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions frontends/rioterm/src/bindings/kitty_keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,12 +495,18 @@ fn alternate_keys_payload(
/// while a non-Latin layout is active. Plain and shifted keys are text
/// input, so they never fall back.
pub fn base_layout_binding_key(key: &KeyEvent, mods: ModifiersState) -> Option<Key> {
base_layout_fallback_key(&key.logical_key, key.physical_key, mods)
base_layout_fallback_key(
&key.logical_key,
key.physical_key,
key.base_layout_key(),
mods,
)
}

fn base_layout_fallback_key(
logical_key: &Key,
physical_key: PhysicalKey,
layout_resolved: Option<Key>,
mods: ModifiersState,
) -> Option<Key> {
if !(mods.control_key() || mods.alt_key() || mods.super_key()) {
Expand All @@ -519,6 +525,14 @@ fn base_layout_fallback_key(
Key::Unidentified(_) => {}
_ => return None,
}
// Prefer the platform's resolution through the user's real layouts
// (another xkb group, the macOS ASCII-capable layout); fall back to
// the key's position in the standard PC-101 layout.
if let Some(Key::Character(ch)) = layout_resolved {
if ch.is_ascii() && ch.chars().count() == 1 {
return Some(Key::Character(ch.to_lowercase().into()));
}
}
let base = char::from_u32(base_layout_key(physical_key)?)?;
Some(Key::Character(base.to_string().into()))
}
Expand Down Expand Up @@ -588,26 +602,53 @@ mod tests {
use super::*;
use rio_window::keyboard::NativeKey;

#[test]
fn base_layout_fallback_prefers_platform_resolution() {
let cyrillic = Key::Character("с".into());
// Dvorak-style: the layout resolves to a different Latin key than
// the PC-101 position.
let physical = PhysicalKey::Code(KeyCode::KeyC);
assert_eq!(
base_layout_fallback_key(
&cyrillic,
physical,
Some(Key::Character("j".into())),
ModifiersState::CONTROL
),
Some(Key::Character("j".into()))
);
// A non-ASCII or multi-char resolution falls back to PC-101.
assert_eq!(
base_layout_fallback_key(
&cyrillic,
physical,
Some(Key::Character("ж".into())),
ModifiersState::CONTROL
),
Some(Key::Character("c".into()))
);
}

#[test]
fn base_layout_fallback_requires_command_modifier() {
let cyrillic = Key::Character("с".into());
let physical = PhysicalKey::Code(KeyCode::KeyC);
let base = Key::Character("c".into());
assert_eq!(
base_layout_fallback_key(&cyrillic, physical, ModifiersState::CONTROL),
base_layout_fallback_key(&cyrillic, physical, None, ModifiersState::CONTROL),
Some(base.clone())
);
assert_eq!(
base_layout_fallback_key(&cyrillic, physical, ModifiersState::SUPER),
base_layout_fallback_key(&cyrillic, physical, None, ModifiersState::SUPER),
Some(base)
);
// Plain and shifted typing never falls back.
assert_eq!(
base_layout_fallback_key(&cyrillic, physical, ModifiersState::empty()),
base_layout_fallback_key(&cyrillic, physical, None, ModifiersState::empty()),
None
);
assert_eq!(
base_layout_fallback_key(&cyrillic, physical, ModifiersState::SHIFT),
base_layout_fallback_key(&cyrillic, physical, None, ModifiersState::SHIFT),
None
);
}
Expand All @@ -618,7 +659,7 @@ mod tests {
let ctrl_alt = ModifiersState::CONTROL | ModifiersState::ALT;
let unidentified = Key::Unidentified(NativeKey::Unidentified);
assert_eq!(
base_layout_fallback_key(&unidentified, physical, ctrl_alt),
base_layout_fallback_key(&unidentified, physical, None, ctrl_alt),
Some(Key::Character("n".into()))
);
let typed = Key::Character("ń".into());
Expand All @@ -628,7 +669,7 @@ mod tests {
Some(Key::Character("n".into()))
};
assert_eq!(
base_layout_fallback_key(&typed, physical, ctrl_alt),
base_layout_fallback_key(&typed, physical, None, ctrl_alt),
expected
);
}
Expand Down
19 changes: 19 additions & 0 deletions rio-window/src/platform/modifier_supplement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ pub trait KeyEventExtModifierSupplement {
/// key as `Character` according to the current keyboard layout. This value
/// cannot be `Dead`.
fn key_without_modifiers(&self) -> Key;

/// The key resolved against a Latin (ASCII-capable) keyboard layout,
/// ignoring all modifiers.
///
/// When the active layout cannot produce ASCII for this key (Cyrillic,
/// Greek, Arabic, ...), this resolves the key through a layout that can:
/// another configured xkb layout group on Linux, the ASCII-capable
/// keyboard layout on macOS. Useful for matching shortcuts such as
/// `Ctrl+Shift+V` independently of the active layout.
///
/// Returns `None` when no configured layout produces ASCII for this key,
/// or on platforms without layout introspection (Windows); callers may
/// then fall back to the key's position in the standard PC-101 layout.
fn base_layout_key(&self) -> Option<Key>;
}

impl KeyEventExtModifierSupplement for KeyEvent {
Expand All @@ -35,4 +49,9 @@ impl KeyEventExtModifierSupplement for KeyEvent {
fn key_without_modifiers(&self) -> Key {
self.platform_specific.key_without_modifiers.clone()
}

#[inline]
fn base_layout_key(&self) -> Option<Key> {
self.platform_specific.base_layout_key.clone()
}
}
33 changes: 33 additions & 0 deletions rio-window/src/platform_impl/linux/common/xkb/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,39 @@ impl XkbKeymap {
pub fn key_repeats(&mut self, keycode: xkb_keycode_t) -> bool {
unsafe { (XKBH.xkb_keymap_key_repeats)(self.keymap.as_ptr(), keycode) == 1 }
}

/// The key resolved against the first layout group that produces ASCII
/// for this key, checking `active_layout` first. This mirrors xkb's
/// group fallback for shortcuts: a Cyrillic layout resolves through the
/// Latin group configured next to it. `None` when no group produces an
/// ASCII character.
pub fn base_layout_key(
&mut self,
keycode: xkb_keycode_t,
active_layout: xkb_layout_index_t,
) -> Option<Key> {
let num_layouts = unsafe {
(XKBH.xkb_keymap_num_layouts_for_key)(self.keymap.as_ptr(), keycode)
};
let layouts = std::iter::once(active_layout)
.chain((0..num_layouts).filter(|&layout| layout != active_layout));
for layout in layouts {
let keysym = self.first_keysym_by_level(layout, keycode);
if keysym == 0 {
continue;
}
let codepoint = unsafe { (XKBH.xkb_keysym_to_utf32)(keysym) };
let Some(ch) = char::from_u32(codepoint) else {
continue;
};
if ch.is_ascii_graphic() {
return Some(Key::Character(smol_str::SmolStr::new(
ch.to_ascii_lowercase().to_string(),
)));
}
}
None
}
}

impl Drop for XkbKeymap {
Expand Down
5 changes: 5 additions & 0 deletions rio-window/src/platform_impl/linux/common/xkb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ impl<'a> KeyContext<'a> {
state: ElementState,
repeat: bool,
) -> KeyEvent {
let base_layout_key = {
let active_layout = self.state.layout(keycode);
self.keymap.base_layout_key(keycode, active_layout)
};
let mut event = KeyEventResults::new(
self,
keycode,
Expand All @@ -224,6 +228,7 @@ impl<'a> KeyContext<'a> {
let platform_specific = KeyEventExtra {
text_with_all_modifiers,
key_without_modifiers,
base_layout_key,
};

KeyEvent {
Expand Down
1 change: 1 addition & 0 deletions rio-window/src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ impl Window {
pub struct KeyEventExtra {
pub text_with_all_modifiers: Option<SmolStr>,
pub key_without_modifiers: Key,
pub base_layout_key: Option<Key>,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
Expand Down
1 change: 1 addition & 0 deletions rio-window/src/platform_impl/macos/app_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ impl ApplicationDelegate {
platform_specific: KeyEventExtra {
text_with_all_modifiers: character.map(SmolStr::new),
key_without_modifiers: logical_key,
base_layout_key: None,
},
}
}
Expand Down
49 changes: 41 additions & 8 deletions rio-window/src/platform_impl/macos/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,44 @@ use crate::platform_impl::platform::ffi;
pub struct KeyEventExtra {
pub text_with_all_modifiers: Option<SmolStr>,
pub key_without_modifiers: Key,
pub base_layout_key: Option<Key>,
}

/// Ignores ALL modifiers.
pub fn get_modifierless_char(scancode: u16) -> Key {
let input_source = unsafe { ffi::TISCopyCurrentKeyboardLayoutInputSource() };
if input_source.is_null() {
tracing::error!("`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr");
return Key::Unidentified(NativeKey::MacOS(scancode));
}
char_for_input_source(scancode, input_source)
}

/// Like [`get_modifierless_char`] but resolved against the current
/// ASCII-capable keyboard layout, so a Latin key comes back even while a
/// non-Latin layout is active. `None` when the resolution fails or yields
/// no character.
pub fn get_base_layout_char(scancode: u16) -> Option<Key> {
let input_source =
unsafe { ffi::TISCopyCurrentASCIICapableKeyboardLayoutInputSource() };
if input_source.is_null() {
tracing::error!(
"`TISCopyCurrentASCIICapableKeyboardLayoutInputSource` returned null ptr"
);
return None;
}
match char_for_input_source(scancode, input_source) {
key @ Key::Character(_) => Some(key),
_ => None,
}
}

/// Translate `scancode` with no modifiers through `input_source`, releasing
/// the source before returning.
fn char_for_input_source(scancode: u16, input_source: ffi::TISInputSourceRef) -> Key {
let mut string = [0; 16];
let input_source;
let layout;
unsafe {
input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource();
if input_source.is_null() {
tracing::error!(
"`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr"
);
return Key::Unidentified(NativeKey::MacOS(scancode));
}
let layout_data = ffi::TISGetInputSourceProperty(
input_source,
ffi::kTISPropertyUnicodeKeyLayoutData,
Expand Down Expand Up @@ -174,6 +197,15 @@ pub(crate) fn create_key_event(
(key_from_code.clone(), key_from_code)
};

// Named keys are layout-independent; character keys on an ASCII-capable
// layout already carry their Latin key, so the extra translation is only
// needed when the active layout produced a non-ASCII character.
let base_layout_key = match &key_without_modifiers {
Key::Character(ch) if ch.is_ascii() => Some(key_without_modifiers.clone()),
Key::Character(_) | Key::Unidentified(_) => get_base_layout_char(scancode),
_ => None,
};

let text = if is_press {
logical_key.to_text().map(SmolStr::new)
} else {
Expand All @@ -192,6 +224,7 @@ pub(crate) fn create_key_event(
platform_specific: KeyEventExtra {
text_with_all_modifiers,
key_without_modifiers,
base_layout_key,
},
}
}
Expand Down
2 changes: 2 additions & 0 deletions rio-window/src/platform_impl/macos/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ extern "C" {

pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef;

pub fn TISCopyCurrentASCIICapableKeyboardLayoutInputSource() -> TISInputSourceRef;

pub fn LMGetKbdType() -> u8;

#[allow(non_snake_case)]
Expand Down
1 change: 1 addition & 0 deletions rio-window/src/platform_impl/orbital/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ impl<T: 'static> EventLoop<T> {
platform_specific: KeyEventExtra {
key_without_modifiers,
text_with_all_modifiers,
base_layout_key: None,
},
},
is_synthetic: false,
Expand Down
1 change: 1 addition & 0 deletions rio-window/src/platform_impl/orbital/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,5 @@ impl VideoModeHandle {
pub struct KeyEventExtra {
pub key_without_modifiers: Key,
pub text_with_all_modifiers: Option<SmolStr>,
pub base_layout_key: Option<Key>,
}
1 change: 1 addition & 0 deletions rio-window/src/platform_impl/windows/keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ impl PartialKeyEventInfo {
platform_specific: KeyEventExtra {
text_with_all_modifiers: char_with_all_modifiers,
key_without_modifiers: self.key_without_modifiers,
base_layout_key: None,
},
}
}
Expand Down
1 change: 1 addition & 0 deletions rio-window/src/platform_impl/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub type OsError = std::io::Error;
pub struct KeyEventExtra {
pub text_with_all_modifiers: Option<SmolStr>,
pub key_without_modifiers: Key,
pub base_layout_key: Option<Key>,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down
Loading