add keyboard mapper

This commit is contained in:
Junkui Zhang 2025-08-19 23:52:37 +08:00
parent ca13d025ed
commit 1d088ecebe
6 changed files with 156 additions and 16 deletions

View file

@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
WindowHandle, WindowId, WindowInvalidator,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@ -263,6 +263,7 @@ pub struct App {
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
pending_effects: VecDeque<Effect>,
@ -312,6 +313,7 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_layout = platform.keyboard_layout();
let keyboard_mapper = platform.keyboard_mapper();
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(App {
@ -337,6 +339,7 @@ impl App {
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout,
keyboard_mapper,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
pending_notifications: FxHashSet::default(),
@ -376,6 +379,7 @@ impl App {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_layout = cx.platform.keyboard_layout();
cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
@ -424,6 +428,11 @@ impl App {
self.keyboard_layout.as_ref()
}
/// Get the current keyboard mapper.
pub fn keyboard_mapper(&self) -> &dyn PlatformKeyboardMapper {
self.keyboard_mapper.as_ref()
}
/// Invokes a handler when the current keyboard layout changes
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
where

View file

@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn compositor_name(&self) -> &'static str {
""
@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
}
/// A handle to a platform's display, e.g. a monitor or laptop screen.

View file

@ -1,3 +1,5 @@
use crate::{KeybindingKeystroke, Keystroke};
/// A trait for platform-specific keyboard layouts
pub trait PlatformKeyboardLayout {
/// Get the keyboard layout ID, which should be unique to the layout
@ -5,3 +7,9 @@ pub trait PlatformKeyboardLayout {
/// Get the keyboard layout display name
fn name(&self) -> &str;
}
/// A trait for platform-specific keyboard mappings
pub trait PlatformKeyboardMapper {
/// Map a key equivalent to its platform-specific representation
fn map_key_equivalent(&self, keystroke: Keystroke) -> KeybindingKeystroke;
}

View file

@ -300,13 +300,11 @@ impl Keystroke {
impl KeybindingKeystroke {
/// Create a new keybinding keystroke from the given keystroke
pub fn new(keystroke: Keystroke) -> Self {
let (key, modifiers) = to_unshifted_key(&keystroke.key, &keystroke.modifiers);
let inner = keystroke.into_shifted();
pub fn new(inner: Keystroke) -> Self {
KeybindingKeystroke {
inner,
key,
modifiers,
modifiers: inner.modifiers,
key: inner.key.clone(),
}
}
}

View file

@ -1,22 +1,28 @@
use std::borrow::Cow;
use anyhow::Result;
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, 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,
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 windows_core::HSTRING;
use crate::{Modifiers, PlatformKeyboardLayout};
use crate::{
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
};
pub(crate) struct WindowsKeyboardLayout {
id: String,
name: String,
}
pub(crate) struct WindowsKeyboardMapper;
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
fn id(&self) -> &str {
&self.id
@ -27,6 +33,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
}
}
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
fn map_key_equivalent(&self, mut keystroke: Keystroke) -> KeybindingKeystroke {
let Some((vkey, shift)) = key_needs_processing(&keystroke.key) else {
return KeybindingKeystroke::new(keystroke);
};
if shift && keystroke.modifiers.shift {
log::warn!(
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
keystroke.key
);
keystroke.modifiers.shift = false;
}
// translate to unshifted key first
let Some(key) = get_key_from_vkey(vkey) else {
log::error!(
"Failed to map key equivalent '{:?}' to a valid key",
keystroke
);
return KeybindingKeystroke::new(keystroke);
};
let modifiers = Modifiers {
control: keystroke.modifiers.control,
alt: keystroke.modifiers.alt,
shift,
platform: keystroke.modifiers.platform,
function: keystroke.modifiers.function,
};
keystroke.key = if shift {
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
if scan_code == 0 {
log::error!(
"Failed to map keystroke {:?} with virtual key '{:?}' to a scan code",
keystroke,
vkey
);
return KeybindingKeystroke::new(keystroke);
}
let Some(shifted_key) = get_shifted_key(vkey, scan_code) else {
log::error!(
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
keystroke,
vkey
);
return KeybindingKeystroke::new(keystroke);
};
shifted_key
} else {
key.clone()
};
KeybindingKeystroke {
inner: keystroke,
modifiers,
key,
}
}
}
impl WindowsKeyboardLayout {
pub(crate) fn new() -> Result<Self> {
let mut buffer = [0u16; KL_NAMELENGTH as usize];
@ -48,6 +113,12 @@ impl WindowsKeyboardLayout {
}
}
impl WindowsKeyboardMapper {
pub(crate) fn new() -> Self {
Self
}
}
pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY,
scan_code: u32,
@ -140,3 +211,51 @@ pub(crate) fn generate_key_char(
_ => None,
}
}
fn key_needs_processing(key: &str) -> Option<(VIRTUAL_KEY, bool)> {
match key {
"`" => Some((VK_OEM_3, false)),
"~" => Some((VK_OEM_3, true)),
"1" => Some((VK_1, false)),
"!" => Some((VK_1, true)),
"2" => Some((VK_2, false)),
"@" => Some((VK_2, true)),
"3" => Some((VK_3, false)),
"#" => Some((VK_3, true)),
"4" => Some((VK_4, false)),
"$" => Some((VK_4, true)),
"5" => Some((VK_5, false)),
"%" => Some((VK_5, true)),
"6" => Some((VK_6, false)),
"^" => Some((VK_6, true)),
"7" => Some((VK_7, false)),
"&" => Some((VK_7, true)),
"8" => Some((VK_8, false)),
"*" => Some((VK_8, true)),
"9" => Some((VK_9, false)),
"(" => Some((VK_9, true)),
"0" => Some((VK_0, false)),
")" => Some((VK_0, true)),
"-" => Some((VK_OEM_MINUS, false)),
"_" => Some((VK_OEM_MINUS, true)),
"=" => Some((VK_OEM_PLUS, false)),
"+" => Some((VK_OEM_PLUS, true)),
"[" => Some((VK_OEM_4, false)),
"{" => Some((VK_OEM_4, true)),
"]" => Some((VK_OEM_6, false)),
"}" => Some((VK_OEM_6, true)),
"\\" => Some((VK_OEM_5, false)),
"|" => Some((VK_OEM_5, true)),
";" => Some((VK_OEM_1, false)),
":" => Some((VK_OEM_1, true)),
"'" => Some((VK_OEM_7, false)),
"\"" => Some((VK_OEM_7, true)),
"," => Some((VK_OEM_COMMA, false)),
"<" => Some((VK_OEM_COMMA, true)),
"." => Some((VK_OEM_PERIOD, false)),
">" => Some((VK_OEM_PERIOD, true)),
"/" => Some((VK_OEM_2, false)),
"?" => Some((VK_OEM_2, true)),
_ => None,
}
}

View file

@ -351,6 +351,10 @@ impl Platform for WindowsPlatform {
)
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(WindowsKeyboardMapper::new())
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
}