Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
372 lines
11 KiB
Rust
372 lines
11 KiB
Rust
use anyhow::Result;
|
|
use collections::HashMap;
|
|
use windows::Win32::UI::{
|
|
Input::KeyboardAndMouse::{
|
|
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
|
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
|
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
|
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
|
},
|
|
WindowsAndMessaging::KL_NAMELENGTH,
|
|
};
|
|
|
|
use gpui::{
|
|
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
|
};
|
|
|
|
pub(crate) struct WindowsKeyboardLayout {
|
|
id: String,
|
|
name: String,
|
|
}
|
|
|
|
pub(crate) struct WindowsKeyboardMapper {
|
|
key_to_vkey: HashMap<String, (u16, bool)>,
|
|
vkey_to_key: HashMap<u16, String>,
|
|
vkey_to_shifted: HashMap<u16, String>,
|
|
}
|
|
|
|
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
|
fn id(&self) -> &str {
|
|
&self.id
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
}
|
|
|
|
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
|
fn map_key_equivalent(
|
|
&self,
|
|
mut keystroke: Keystroke,
|
|
use_key_equivalents: bool,
|
|
) -> KeybindingKeystroke {
|
|
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
|
else {
|
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
|
};
|
|
if shifted_key && keystroke.modifiers.shift {
|
|
log::warn!(
|
|
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
|
keystroke.key
|
|
);
|
|
}
|
|
|
|
let shift = shifted_key || keystroke.modifiers.shift;
|
|
keystroke.modifiers.shift = false;
|
|
|
|
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
|
log::error!(
|
|
"Failed to map key equivalent '{:?}' to a valid key",
|
|
keystroke
|
|
);
|
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
|
};
|
|
|
|
keystroke.key = if shift {
|
|
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
|
log::error!(
|
|
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
|
keystroke,
|
|
vkey
|
|
);
|
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
|
};
|
|
shifted_key
|
|
} else {
|
|
key.clone()
|
|
};
|
|
|
|
let modifiers = Modifiers {
|
|
shift,
|
|
..keystroke.modifiers
|
|
};
|
|
|
|
KeybindingKeystroke::new(keystroke, modifiers, key)
|
|
}
|
|
|
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl WindowsKeyboardLayout {
|
|
pub(crate) fn new() -> Result<Self> {
|
|
let mut buffer = [0u16; KL_NAMELENGTH as usize]; // KL_NAMELENGTH includes the null terminator
|
|
unsafe { GetKeyboardLayoutNameW(&mut buffer)? };
|
|
let id = String::from_utf16_lossy(&buffer[..buffer.len() - 1]); // Remove the null terminator
|
|
let entry = windows_registry::LOCAL_MACHINE.open(format!(
|
|
"System\\CurrentControlSet\\Control\\Keyboard Layouts\\{id}"
|
|
))?;
|
|
let name = entry.get_string("Layout Text")?;
|
|
Ok(Self { id, name })
|
|
}
|
|
|
|
pub(crate) fn unknown() -> Self {
|
|
Self {
|
|
id: "unknown".to_string(),
|
|
name: "unknown".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WindowsKeyboardMapper {
|
|
pub(crate) fn new() -> Self {
|
|
let mut key_to_vkey = HashMap::default();
|
|
let mut vkey_to_key = HashMap::default();
|
|
let mut vkey_to_shifted = HashMap::default();
|
|
for vkey in CANDIDATE_VKEYS {
|
|
if let Some(key) = get_key_from_vkey(*vkey) {
|
|
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
|
vkey_to_key.insert(vkey.0, key);
|
|
}
|
|
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
|
if scan_code == 0 {
|
|
continue;
|
|
}
|
|
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
|
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
|
vkey_to_shifted.insert(vkey.0, shifted_key);
|
|
}
|
|
}
|
|
Self {
|
|
key_to_vkey,
|
|
vkey_to_key,
|
|
vkey_to_shifted,
|
|
}
|
|
}
|
|
|
|
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
|
if use_key_equivalents {
|
|
get_vkey_from_key_with_us_layout(key)
|
|
} else {
|
|
self.key_to_vkey.get(key).cloned()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get_keystroke_key(
|
|
vkey: VIRTUAL_KEY,
|
|
scan_code: u32,
|
|
modifiers: &mut Modifiers,
|
|
) -> Option<String> {
|
|
if modifiers.shift && need_to_convert_to_shifted_key(vkey) {
|
|
get_shifted_key(vkey, scan_code).inspect(|_| {
|
|
modifiers.shift = false;
|
|
})
|
|
} else {
|
|
get_key_from_vkey(vkey)
|
|
}
|
|
}
|
|
|
|
fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option<String> {
|
|
let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) };
|
|
if key_data == 0 {
|
|
return None;
|
|
}
|
|
|
|
// The high word contains dead key flag, the low word contains the character
|
|
let key = char::from_u32(key_data & 0xFFFF)?;
|
|
|
|
Some(key.to_ascii_lowercase().to_string())
|
|
}
|
|
|
|
#[inline]
|
|
fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
|
|
matches!(
|
|
vkey,
|
|
VK_OEM_3
|
|
| VK_OEM_MINUS
|
|
| VK_OEM_PLUS
|
|
| VK_OEM_4
|
|
| VK_OEM_5
|
|
| VK_OEM_6
|
|
| VK_OEM_1
|
|
| VK_OEM_7
|
|
| VK_OEM_COMMA
|
|
| VK_OEM_PERIOD
|
|
| VK_OEM_2
|
|
| VK_OEM_102
|
|
| VK_OEM_8
|
|
| VK_ABNT_C1
|
|
| VK_0
|
|
| VK_1
|
|
| VK_2
|
|
| VK_3
|
|
| VK_4
|
|
| VK_5
|
|
| VK_6
|
|
| VK_7
|
|
| VK_8
|
|
| VK_9
|
|
)
|
|
}
|
|
|
|
fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option<String> {
|
|
generate_key_char(vkey, scan_code, false, true, false)
|
|
}
|
|
|
|
pub(crate) fn generate_key_char(
|
|
vkey: VIRTUAL_KEY,
|
|
scan_code: u32,
|
|
control: bool,
|
|
shift: bool,
|
|
alt: bool,
|
|
) -> Option<String> {
|
|
let mut state = [0; 256];
|
|
if control {
|
|
state[VK_CONTROL.0 as usize] = 0x80;
|
|
}
|
|
if shift {
|
|
state[VK_SHIFT.0 as usize] = 0x80;
|
|
}
|
|
if alt {
|
|
state[VK_MENU.0 as usize] = 0x80;
|
|
}
|
|
|
|
let mut buffer = [0; 8];
|
|
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 0x5) };
|
|
|
|
match len {
|
|
len if len > 0 => String::from_utf16(&buffer[..len as usize])
|
|
.ok()
|
|
.filter(|candidate| {
|
|
!candidate.is_empty() && !candidate.chars().next().unwrap().is_control()
|
|
}),
|
|
len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
|
match key {
|
|
// ` => VK_OEM_3
|
|
"`" => Some((VK_OEM_3.0, false)),
|
|
"~" => Some((VK_OEM_3.0, true)),
|
|
"1" => Some((VK_1.0, false)),
|
|
"!" => Some((VK_1.0, true)),
|
|
"2" => Some((VK_2.0, false)),
|
|
"@" => Some((VK_2.0, true)),
|
|
"3" => Some((VK_3.0, false)),
|
|
"#" => Some((VK_3.0, true)),
|
|
"4" => Some((VK_4.0, false)),
|
|
"$" => Some((VK_4.0, true)),
|
|
"5" => Some((VK_5.0, false)),
|
|
"%" => Some((VK_5.0, true)),
|
|
"6" => Some((VK_6.0, false)),
|
|
"^" => Some((VK_6.0, true)),
|
|
"7" => Some((VK_7.0, false)),
|
|
"&" => Some((VK_7.0, true)),
|
|
"8" => Some((VK_8.0, false)),
|
|
"*" => Some((VK_8.0, true)),
|
|
"9" => Some((VK_9.0, false)),
|
|
"(" => Some((VK_9.0, true)),
|
|
"0" => Some((VK_0.0, false)),
|
|
")" => Some((VK_0.0, true)),
|
|
"-" => Some((VK_OEM_MINUS.0, false)),
|
|
"_" => Some((VK_OEM_MINUS.0, true)),
|
|
"=" => Some((VK_OEM_PLUS.0, false)),
|
|
"+" => Some((VK_OEM_PLUS.0, true)),
|
|
"[" => Some((VK_OEM_4.0, false)),
|
|
"{" => Some((VK_OEM_4.0, true)),
|
|
"]" => Some((VK_OEM_6.0, false)),
|
|
"}" => Some((VK_OEM_6.0, true)),
|
|
"\\" => Some((VK_OEM_5.0, false)),
|
|
"|" => Some((VK_OEM_5.0, true)),
|
|
";" => Some((VK_OEM_1.0, false)),
|
|
":" => Some((VK_OEM_1.0, true)),
|
|
"'" => Some((VK_OEM_7.0, false)),
|
|
"\"" => Some((VK_OEM_7.0, true)),
|
|
"," => Some((VK_OEM_COMMA.0, false)),
|
|
"<" => Some((VK_OEM_COMMA.0, true)),
|
|
"." => Some((VK_OEM_PERIOD.0, false)),
|
|
">" => Some((VK_OEM_PERIOD.0, true)),
|
|
"/" => Some((VK_OEM_2.0, false)),
|
|
"?" => Some((VK_OEM_2.0, true)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
|
VK_OEM_3,
|
|
VK_OEM_MINUS,
|
|
VK_OEM_PLUS,
|
|
VK_OEM_4,
|
|
VK_OEM_5,
|
|
VK_OEM_6,
|
|
VK_OEM_1,
|
|
VK_OEM_7,
|
|
VK_OEM_COMMA,
|
|
VK_OEM_PERIOD,
|
|
VK_OEM_2,
|
|
VK_OEM_102,
|
|
VK_OEM_8,
|
|
VK_ABNT_C1,
|
|
VK_0,
|
|
VK_1,
|
|
VK_2,
|
|
VK_3,
|
|
VK_4,
|
|
VK_5,
|
|
VK_6,
|
|
VK_7,
|
|
VK_8,
|
|
VK_9,
|
|
];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::WindowsKeyboardMapper;
|
|
use gpui::{Keystroke, Modifiers, PlatformKeyboardMapper};
|
|
|
|
#[test]
|
|
fn test_keyboard_mapper() {
|
|
let mapper = WindowsKeyboardMapper::new();
|
|
|
|
// Normal case
|
|
let keystroke = Keystroke {
|
|
modifiers: Modifiers::control(),
|
|
key: "a".to_string(),
|
|
key_char: None,
|
|
};
|
|
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
|
assert_eq!(*mapped.inner(), keystroke);
|
|
assert_eq!(mapped.key(), "a");
|
|
assert_eq!(*mapped.modifiers(), Modifiers::control());
|
|
|
|
// Shifted case, ctrl-$
|
|
let keystroke = Keystroke {
|
|
modifiers: Modifiers::control(),
|
|
key: "$".to_string(),
|
|
key_char: None,
|
|
};
|
|
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
|
assert_eq!(*mapped.inner(), keystroke);
|
|
assert_eq!(mapped.key(), "4");
|
|
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
|
|
|
|
// Shifted case, but shift is true
|
|
let keystroke = Keystroke {
|
|
modifiers: Modifiers::control_shift(),
|
|
key: "$".to_string(),
|
|
key_char: None,
|
|
};
|
|
let mapped = mapper.map_key_equivalent(keystroke, true);
|
|
assert_eq!(mapped.inner().modifiers, Modifiers::control());
|
|
assert_eq!(mapped.key(), "4");
|
|
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
|
|
|
|
// Windows style
|
|
let keystroke = Keystroke {
|
|
modifiers: Modifiers::control_shift(),
|
|
key: "4".to_string(),
|
|
key_char: None,
|
|
};
|
|
let mapped = mapper.map_key_equivalent(keystroke, true);
|
|
assert_eq!(mapped.inner().modifiers, Modifiers::control());
|
|
assert_eq!(mapped.inner().key, "$");
|
|
assert_eq!(mapped.key(), "4");
|
|
assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
|
|
}
|
|
}
|