Keys are named by the active layout, as 1.12.1 names them (#748)
Letter and punctuation keys were named by their US position, so on AZERTY the key labelled Z bound as W, and Ctrl+A sat on the key labelled Q. 1.12.1 on Windows names a key from the VK its layout produced. `VK_A`–`VK_Z` and digits are named as themselves, and other keys by `MapVirtualKeyA` (`0x42da39`); a key with no character is dropped (`0x42da49`). The EditBox's Ctrl chords switch on that same name (`0x77b1fb`). The Mac client names keys by `KeyTranslate`. Now: - Windows takes the same path through `MapVirtualKeyExW`. - The Mac names keys through the current layout, or the ASCII-capable layout it pairs with. - Linux follows one documented rule. - Bindings, Key Bindings capture and Ctrl/Cmd+A/C/X/V all follow the layout. - A character above U+00FF names its key, as a deviation. Tests cover AZERTY, Hebrew, Greek, Lithuanian, Turkish, Bulgarian, Hungarian and Dvorak names, layout switches and chords. Fixes #621 Fixes #746 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e9f0e89a0b
commit
be9e3d8aac
Generated
+1
@@ -546,6 +546,7 @@ dependencies = [
|
||||
"toml",
|
||||
"wgpu",
|
||||
"windows-sys 0.59.0",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -50,6 +50,9 @@ toml = "0.8"
|
||||
avian3d = "0.6"
|
||||
# Shapes and rasterizes `FontString` text standalone (`ui_text`), at bevy_text's own 0.16.
|
||||
cosmic-text = "0.16"
|
||||
# The layout's unshifted character per key (`KeyEventExtModifierSupplement`), which bevy's
|
||||
# `KeyboardInput` drops; bevy_winit's own 0.30, whose features it sets.
|
||||
winit = { version = "0.30", default-features = false }
|
||||
# `cosmic-text`'s font database, named by `ui_text`; must stay the version it depends on.
|
||||
fontdb = "0.23"
|
||||
# The mixer. Its `cpal` backend stays off on every target: `sound::output` is our own backend.
|
||||
@@ -94,12 +97,14 @@ objc2-foundation = { version = "0.2", features = ["NSArray", "NSData", "NSGeomet
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
cpal = "0.17"
|
||||
|
||||
# Windows CPU clocks for `perf::clock`; `GetLocalTime` for the log stamp (`ui_chat::logging`).
|
||||
# Windows CPU clocks for `perf::clock`; `GetLocalTime` for the log stamp (`ui_chat::logging`); the
|
||||
# layout's virtual keys for `bindings::layout`.
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
] }
|
||||
|
||||
# The login cycle's fake realmd runs the server half of SRP6 (`net::io::cycle_tests`).
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! Dispatch ([`latch_and_dispatch`], in [`crate::ui_script::UiInput`] after the UI key feed), as
|
||||
//! `CBindings::ExecuteBinding` (`0x4b7990`) and `RunCommand` (`0x4b7b50`) do:
|
||||
//! - a press probes its exact chord, then once more with its leftmost modifier dropped
|
||||
//! ([`Chord::fallback`]); Super held matches nothing;
|
||||
//! ([`Chord::fallback`]);
|
||||
//! - the resolved command's body runs with `keystate = "down"`, and the base key's release runs a
|
||||
//! `runOnUp` body again with `"up"`. UI focus suppresses presses and releases nothing already
|
||||
//! held; a latch ends only on its base key's release, the stuck-latch sweep (OS focus loss, the
|
||||
@@ -15,6 +15,11 @@
|
||||
//! - what the bodies call reaches the engine through the Lua binding functions
|
||||
//! ([`benilla_ui::script::BindingInput`]) as [`BindingsState`]'s [`Input`]s.
|
||||
//!
|
||||
//! Deviation: a press with Super (Cmd) held runs no binding, so a system chord such as Cmd+Tab or
|
||||
//! Cmd+Q never acts in game. The reference has no such modifier and runs the bare key's binding:
|
||||
//! the Mac enqueues the key with Command held and skips only its text (`0x8a519`), and Windows
|
||||
//! hands `VK_LWIN`/`VK_RWIN` to the layout arm (`0x42da39`) like any key.
|
||||
//!
|
||||
//! Persistence: `benilla-config/bindings/account.txt` and `<Realm>-<Char>.txt` ([`store`]); the
|
||||
//! character file's existence is the character-set state, as in the reference.
|
||||
|
||||
@@ -30,10 +35,12 @@ use crate::char_select::InWorldGated;
|
||||
use crate::ui_script::{PlayerUiHover, PointerOverUiPanel, UiKeyboardCapture};
|
||||
|
||||
pub(crate) mod chord;
|
||||
mod layout;
|
||||
mod script_input;
|
||||
mod store;
|
||||
|
||||
use chord::{BindKey, Chord};
|
||||
pub(crate) use layout::LayoutNames;
|
||||
pub(crate) use script_input::Input;
|
||||
|
||||
/// The chord-to-command map, rebuilt whenever the engine table's generation moves. Probe it
|
||||
@@ -69,10 +76,10 @@ impl BindingDispatch {
|
||||
/// [`Input`]s the running bodies' Lua calls set.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct BindingsState {
|
||||
/// Live latches, (base key, command): a press not yet released, whose release runs the
|
||||
/// Live latches, (base input, command): a press not yet released, whose release runs the
|
||||
/// command's `runOnUp` half. The reference replays the press-time chord at key-up (`0x483bd0`);
|
||||
/// latching the resolved command is equivalent.
|
||||
latched: Vec<(BindKey, String)>,
|
||||
latched: Vec<(Held, String)>,
|
||||
/// The held bits a Start set and no Stop has cleared.
|
||||
held: Vec<Input>,
|
||||
/// Inputs whose Start, or whose one-shot, came this frame (the press edge).
|
||||
@@ -89,6 +96,15 @@ pub(crate) struct BindingsState {
|
||||
down: Vec<KeyCode>,
|
||||
}
|
||||
|
||||
/// The physical input a latch waits on: a keyboard key by its code, not its name, so its release
|
||||
/// ends the latch whatever the layout names it by then.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Held {
|
||||
Key(KeyCode),
|
||||
Mouse(MouseButton),
|
||||
Wheel,
|
||||
}
|
||||
|
||||
impl BindingsState {
|
||||
/// Is a held bit set right now (the reference's `[InputControl+4]`)?
|
||||
pub(crate) fn pressed(&self, i: Input) -> bool {
|
||||
@@ -140,7 +156,8 @@ pub(crate) struct BindingsPlugin;
|
||||
|
||||
impl Plugin for BindingsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<BindingDispatch>()
|
||||
app.add_plugins(layout::plugin)
|
||||
.init_resource::<BindingDispatch>()
|
||||
.init_resource::<BindingsState>()
|
||||
.init_resource::<BindingFiles>()
|
||||
.add_systems(
|
||||
@@ -308,6 +325,8 @@ impl WheelNotches {
|
||||
fn latch_and_dispatch(
|
||||
mut script: Option<NonSendMut<UiScript>>,
|
||||
mut keyboard: MessageReader<KeyboardInput>,
|
||||
// What the active layout names a letter, digit or punctuation key.
|
||||
layout: Res<LayoutNames>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
scroll: Res<AccumulatedMouseScroll>,
|
||||
@@ -357,8 +376,9 @@ fn latch_and_dispatch(
|
||||
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
||||
let alt = keys.pressed(KeyCode::AltLeft) || keys.pressed(KeyCode::AltRight);
|
||||
let sup = keys.pressed(KeyCode::SuperLeft) || keys.pressed(KeyCode::SuperRight);
|
||||
// The dev overlays' plane (`modkeys::dev_chord`; the `sup` gate covers its Super arm), which
|
||||
// costs the keyboard its fallback probe (`BindingDispatch::resolve`). Only in a build with dev
|
||||
// The dev overlays' plane, Ctrl+Shift without Alt (`modkeys::dev_chord`, which Super blocks
|
||||
// too, as the `sup` gate below keeps any Super-held press from dispatching), which costs the
|
||||
// keyboard its fallback probe (`BindingDispatch::resolve`). Only in a build with dev
|
||||
// affordances: a player build keeps the reference's `CTRL-SHIFT-P` fallback to `SHIFT-P`.
|
||||
let dev_plane = ctrl && shift && !alt && crate::run_mode::dev_affordances();
|
||||
|
||||
@@ -374,7 +394,7 @@ fn latch_and_dispatch(
|
||||
// reference's `0x424790` wipe, so a key held across an alt-tab re-latches on its next repeat.
|
||||
state
|
||||
.down
|
||||
.retain(|&kc| physically_down(BindKey::Key(kc), &keys, &buttons));
|
||||
.retain(|&kc| physically_down(Held::Key(kc), &keys, &buttons));
|
||||
|
||||
// ── Keyboard ── press edges run their command, gated on ownership and the capture arm;
|
||||
// release edges unlatch and run the `runOnUp` up-half.
|
||||
@@ -402,25 +422,31 @@ fn latch_and_dispatch(
|
||||
if !repeat {
|
||||
state.down.push(key);
|
||||
}
|
||||
// `sup`: the Super deviation (module doc).
|
||||
if (typing && !arrow_exempt) || eaten || sup || repeat {
|
||||
continue;
|
||||
}
|
||||
if state.latched.iter().any(|(k, _)| *k == BindKey::Key(key)) {
|
||||
if state.latched.iter().any(|(k, _)| *k == Held::Key(key)) {
|
||||
continue; // already latched (missed release would double-latch)
|
||||
}
|
||||
// Named under the layout this press was typed in; a key 1.12 cannot name binds
|
||||
// nothing.
|
||||
let Some(name) = chord::key_token(ev.key_code, &layout) else {
|
||||
continue;
|
||||
};
|
||||
let chord = Chord {
|
||||
alt,
|
||||
ctrl,
|
||||
shift,
|
||||
key: BindKey::Key(key),
|
||||
key: BindKey::Key(name),
|
||||
};
|
||||
if let Some(command) = dispatch.resolve(chord, dev_plane) {
|
||||
press(&mut state, &mut script, command, BindKey::Key(key));
|
||||
press(&mut state, &mut script, command, Held::Key(key));
|
||||
}
|
||||
}
|
||||
ButtonState::Released => {
|
||||
state.down.retain(|&kc| kc != key);
|
||||
release(&mut state, &mut script, BindKey::Key(key));
|
||||
release(&mut state, &mut script, Held::Key(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,7 +463,7 @@ fn latch_and_dispatch(
|
||||
if buttons.just_pressed(b)
|
||||
&& !sup
|
||||
&& hover.0.is_none()
|
||||
&& !state.latched.iter().any(|(k, _)| *k == BindKey::Mouse(b))
|
||||
&& !state.latched.iter().any(|(k, _)| *k == Held::Mouse(b))
|
||||
{
|
||||
let chord = Chord {
|
||||
alt,
|
||||
@@ -446,11 +472,11 @@ fn latch_and_dispatch(
|
||||
key: BindKey::Mouse(b),
|
||||
};
|
||||
if let Some(command) = dispatch.resolve(chord, false) {
|
||||
press(&mut state, &mut script, command, BindKey::Mouse(b));
|
||||
press(&mut state, &mut script, command, Held::Mouse(b));
|
||||
}
|
||||
}
|
||||
if buttons.just_released(b) {
|
||||
release(&mut state, &mut script, BindKey::Mouse(b));
|
||||
release(&mut state, &mut script, Held::Mouse(b));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,8 +502,8 @@ fn latch_and_dispatch(
|
||||
};
|
||||
if let Some(command) = dispatch.resolve(chord, false) {
|
||||
for _ in 0..steps.unsigned_abs() {
|
||||
press(&mut state, &mut script, command, key);
|
||||
release(&mut state, &mut script, key);
|
||||
press(&mut state, &mut script, command, Held::Wheel);
|
||||
release(&mut state, &mut script, Held::Wheel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,7 +512,7 @@ fn latch_and_dispatch(
|
||||
// unlatches now and runs its up-half. The reference's two bulk clears land here too, since
|
||||
// bevy zeroes `ButtonInput` for both (see the clears above). UI keyboard focus never reaches
|
||||
// here.
|
||||
let mut stuck: Vec<BindKey> = Vec::new();
|
||||
let mut stuck: Vec<Held> = Vec::new();
|
||||
for (k, _) in &state.latched {
|
||||
if !physically_down(*k, &keys, &buttons) && !stuck.contains(k) {
|
||||
stuck.push(*k);
|
||||
@@ -504,32 +530,32 @@ fn latch_and_dispatch(
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this base key physically down, per bevy's button planes? The stuck-latch sweep and the
|
||||
/// Is this input physically down, per bevy's button planes? The stuck-latch sweep and the
|
||||
/// pressed-key reconcile must agree, so both ask here. `ENTER` is down while either physical
|
||||
/// key is, since [`chord::normalize_key`] folds `NUMPADENTER` into it.
|
||||
fn physically_down(
|
||||
key: BindKey,
|
||||
key: Held,
|
||||
keys: &ButtonInput<KeyCode>,
|
||||
buttons: &ButtonInput<MouseButton>,
|
||||
) -> bool {
|
||||
match key {
|
||||
BindKey::Key(KeyCode::Enter) => {
|
||||
Held::Key(KeyCode::Enter) => {
|
||||
keys.pressed(KeyCode::Enter) || keys.pressed(KeyCode::NumpadEnter)
|
||||
}
|
||||
BindKey::Key(kc) => keys.pressed(kc),
|
||||
BindKey::Mouse(b) => buttons.pressed(b),
|
||||
Held::Key(kc) => keys.pressed(kc),
|
||||
Held::Mouse(b) => buttons.pressed(b),
|
||||
// A notch is a press and a release in one frame; it is never "held".
|
||||
BindKey::WheelUp | BindKey::WheelDown => false,
|
||||
Held::Wheel => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// One matching press: the command's body with `keystate = "down"`, and a latch on its base key
|
||||
/// so the release can run the up-half.
|
||||
/// One matching press: the command's body with `keystate = "down"`, and a latch on its input so
|
||||
/// the release can run the up-half.
|
||||
fn press(
|
||||
state: &mut BindingsState,
|
||||
script: &mut Option<NonSendMut<UiScript>>,
|
||||
command: &str,
|
||||
key: BindKey,
|
||||
key: Held,
|
||||
) {
|
||||
if let Some(s) = script.as_mut() {
|
||||
if let Err(e) = s.execute_binding(command, true) {
|
||||
@@ -541,7 +567,7 @@ fn press(
|
||||
|
||||
/// A base key's release: drop its latch and run the command's `runOnUp` up-half, even while
|
||||
/// typing (the reference completes a pressed binding's release regardless of focus).
|
||||
fn release(state: &mut BindingsState, script: &mut Option<NonSendMut<UiScript>>, key: BindKey) {
|
||||
fn release(state: &mut BindingsState, script: &mut Option<NonSendMut<UiScript>>, key: Held) {
|
||||
let mut i = 0;
|
||||
while i < state.latched.len() {
|
||||
if state.latched[i].0 == key {
|
||||
@@ -684,6 +710,7 @@ mod tests {
|
||||
.init_resource::<PointerOverUiPanel>()
|
||||
.init_resource::<BindingsState>()
|
||||
.init_resource::<BindingDispatch>()
|
||||
.init_resource::<LayoutNames>()
|
||||
.add_systems(Update, (sync_dispatch, latch_and_dispatch).chain());
|
||||
app.insert_non_send_resource(script);
|
||||
app
|
||||
@@ -1170,7 +1197,7 @@ mod tests {
|
||||
app.update();
|
||||
assert_eq!(lua_count(&app, "STABS"), 1);
|
||||
assert_eq!(lua_count(&app, "TABS"), 1);
|
||||
// Super is never a binding modifier: a Super-held press builds no chord, so no fallback.
|
||||
// The Super deviation: a Super-held press runs no binding, not even its bare key's.
|
||||
let mut app = harness();
|
||||
press_key(&mut app, KeyCode::SuperLeft);
|
||||
press_key(&mut app, KeyCode::KeyZ);
|
||||
@@ -1178,6 +1205,97 @@ mod tests {
|
||||
assert!(!state(&app).fired(Input::ToggleSheath));
|
||||
}
|
||||
|
||||
/// What the layout names `k`, as `layout::record_layout_names` records it from the key's own
|
||||
/// OS message, ahead of the press.
|
||||
fn layout_key(app: &mut App, k: KeyCode, name: char) {
|
||||
app.world_mut()
|
||||
.resource_mut::<LayoutNames>()
|
||||
.set(k, Some(layout::LayoutName::Char(name)));
|
||||
}
|
||||
|
||||
/// A press with the logical key the OS reports, modifiers applied.
|
||||
fn type_key(app: &mut App, k: KeyCode, logical: &str) {
|
||||
app.world_mut().write_message(KeyboardInput {
|
||||
key_code: k,
|
||||
logical_key: Key::Character(logical.into()),
|
||||
state: bevy::input::ButtonState::Pressed,
|
||||
text: Some(logical.into()),
|
||||
repeat: false,
|
||||
window: Entity::PLACEHOLDER,
|
||||
});
|
||||
}
|
||||
|
||||
/// 1.12 binds a key by what its layout names it: on AZERTY the key where a US W sits is
|
||||
/// `VK_Z` and runs `Z`'s binding, and `W`'s moves to the key labelled W. Shift is a prefix,
|
||||
/// never part of the name.
|
||||
#[test]
|
||||
fn a_key_runs_the_binding_of_what_its_layout_names_it() {
|
||||
let script = core_script();
|
||||
script
|
||||
.run(
|
||||
r#"SetBinding("SHIFT-Z", "TOGGLEUI"); SetBinding("SHIFT-1", "TARGETPREVIOUSENEMY");
|
||||
SetBinding("SHIFT-;", "TARGETNEARESTENEMY")"#,
|
||||
)
|
||||
.expect("bind");
|
||||
let mut app = vm_harness(script);
|
||||
layout_key(&mut app, KeyCode::KeyW, 'Z');
|
||||
layout_key(&mut app, KeyCode::KeyZ, 'W');
|
||||
type_key(&mut app, KeyCode::KeyW, "z");
|
||||
app.update();
|
||||
assert!(
|
||||
state(&app).fired(Input::ToggleSheath),
|
||||
"the key labelled Z is Z"
|
||||
);
|
||||
assert!(!state(&app).pressed(Input::MoveForward));
|
||||
release_key(&mut app, KeyCode::KeyW);
|
||||
type_key(&mut app, KeyCode::KeyZ, "w");
|
||||
app.update();
|
||||
assert!(
|
||||
state(&app).pressed(Input::MoveForward),
|
||||
"the key labelled W is W"
|
||||
);
|
||||
release_key(&mut app, KeyCode::KeyZ);
|
||||
app.update();
|
||||
assert!(
|
||||
!state(&app).pressed(Input::MoveForward),
|
||||
"its release ends it"
|
||||
);
|
||||
// Shift makes the character `Z`, and the chord `SHIFT-Z`.
|
||||
press_key(&mut app, KeyCode::ShiftLeft);
|
||||
type_key(&mut app, KeyCode::KeyW, "Z");
|
||||
app.update();
|
||||
assert_eq!(lua_count(&app, "UITOGGLES"), 1, "SHIFT-Z");
|
||||
release_key(&mut app, KeyCode::KeyW);
|
||||
// Shift+1 makes `!` on a US layout, and is `SHIFT-1`.
|
||||
type_key(&mut app, KeyCode::Digit1, "!");
|
||||
app.update();
|
||||
assert_eq!(lua_count(&app, "STABS"), 1, "SHIFT-1, never SHIFT-!");
|
||||
release_key(&mut app, KeyCode::Digit1);
|
||||
// And Shift+; makes `:`, and is `SHIFT-;`.
|
||||
type_key(&mut app, KeyCode::Semicolon, ":");
|
||||
app.update();
|
||||
assert_eq!(lua_count(&app, "TABS"), 1, "SHIFT-;, never SHIFT-:");
|
||||
}
|
||||
|
||||
/// A key's name follows a layout switch while running: its next press is named under the
|
||||
/// layout it was typed in.
|
||||
#[test]
|
||||
fn a_layout_switch_renames_a_key_at_its_next_press() {
|
||||
let mut app = harness();
|
||||
layout_key(&mut app, KeyCode::KeyW, 'Z');
|
||||
type_key(&mut app, KeyCode::KeyW, "z");
|
||||
app.update();
|
||||
assert!(state(&app).fired(Input::ToggleSheath));
|
||||
release_key(&mut app, KeyCode::KeyW);
|
||||
app.update();
|
||||
// Back on a US layout, the key's next message reports `w`.
|
||||
layout_key(&mut app, KeyCode::KeyW, 'W');
|
||||
type_key(&mut app, KeyCode::KeyW, "w");
|
||||
app.update();
|
||||
assert!(state(&app).pressed(Input::MoveForward));
|
||||
assert!(!state(&app).fired(Input::ToggleSheath));
|
||||
}
|
||||
|
||||
/// The dev plane spends the keyboard's fallback probe and nothing else, asserted on
|
||||
/// [`BindingDispatch::resolve`] itself.
|
||||
#[test]
|
||||
|
||||
@@ -4,15 +4,37 @@
|
||||
//!
|
||||
//! Prefix order is ALT-CTRL-SHIFT (`Blizzard_BindingUI.lua:176-182`; the emitter `0x4b6630` walks
|
||||
//! the table at `0x846bd0`), and it decides which modifier the fallback drops. Super/Cmd is not a
|
||||
//! 1.12 modifier: a chord never carries it and a press with it held never matches.
|
||||
//! 1.12 modifier, so a chord never carries it; that a press with it held runs nothing is
|
||||
//! `crate::bindings`'s deviation.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use bevy::input::mouse::MouseButton;
|
||||
use bevy::prelude::KeyCode;
|
||||
|
||||
/// A bindable base input: a keyboard key, a mouse button, or one wheel direction.
|
||||
use super::layout::{LayoutName, LayoutNames};
|
||||
|
||||
/// A key's 1.12 name: one character (`Z`, `1`, `ù`), or a word from the reference's name table
|
||||
/// (`F1`, `NUMPAD7`, `SPACE`).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub(crate) enum KeyName {
|
||||
Char(char),
|
||||
Word(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KeyName::Char(c) => write!(f, "{c}"),
|
||||
KeyName::Word(w) => f.write_str(w),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bindable base input: a keyboard key by its 1.12 name, a mouse button, or one wheel direction.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub(crate) enum BindKey {
|
||||
Key(KeyCode),
|
||||
Key(KeyName),
|
||||
Mouse(MouseButton),
|
||||
WheelUp,
|
||||
WheelDown,
|
||||
@@ -78,143 +100,107 @@ impl Chord {
|
||||
}
|
||||
}
|
||||
|
||||
/// The 1.12 token for a physical key; `None` for a key the reference names `UNKNOWN` and for the
|
||||
/// modifiers, which are only prefixes (`IsKeyPressIgnoredForBinding`). Both Enters are `ENTER`.
|
||||
pub(crate) fn key_token(k: KeyCode) -> Option<&'static str> {
|
||||
/// The 1.12 name a key press gets: a letter, digit or punctuation key what the active layout
|
||||
/// names it ([`LayoutNames`]), every other key its fixed name. `None` for a key the reference
|
||||
/// names `UNKNOWN` or drops, and for the modifiers, which are only prefixes
|
||||
/// (`IsKeyPressIgnoredForBinding`). Both Enters are `ENTER`.
|
||||
pub(crate) fn key_token(k: KeyCode, layout: &LayoutNames) -> Option<KeyName> {
|
||||
match layout.name(k) {
|
||||
// The lookup folds `a`-`z` up (`0x64b419`), so Turkish `VK_OEM_7`'s `i` is `I`'s key.
|
||||
Some(LayoutName::Char(c)) => Some(KeyName::Char(c.to_ascii_uppercase())),
|
||||
Some(LayoutName::Dropped) => None,
|
||||
None => fixed_name(k),
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixed half of `0x42d800` and the Mac table: every key no layout names
|
||||
/// ([`super::layout::position`]).
|
||||
fn fixed_name(k: KeyCode) -> Option<KeyName> {
|
||||
use KeyCode::*;
|
||||
use KeyName::Word;
|
||||
Some(match k {
|
||||
KeyA => "A",
|
||||
KeyB => "B",
|
||||
KeyC => "C",
|
||||
KeyD => "D",
|
||||
KeyE => "E",
|
||||
KeyF => "F",
|
||||
KeyG => "G",
|
||||
KeyH => "H",
|
||||
KeyI => "I",
|
||||
KeyJ => "J",
|
||||
KeyK => "K",
|
||||
KeyL => "L",
|
||||
KeyM => "M",
|
||||
KeyN => "N",
|
||||
KeyO => "O",
|
||||
KeyP => "P",
|
||||
KeyQ => "Q",
|
||||
KeyR => "R",
|
||||
KeyS => "S",
|
||||
KeyT => "T",
|
||||
KeyU => "U",
|
||||
KeyV => "V",
|
||||
KeyW => "W",
|
||||
KeyX => "X",
|
||||
KeyY => "Y",
|
||||
KeyZ => "Z",
|
||||
Digit1 => "1",
|
||||
Digit2 => "2",
|
||||
Digit3 => "3",
|
||||
Digit4 => "4",
|
||||
Digit5 => "5",
|
||||
Digit6 => "6",
|
||||
Digit7 => "7",
|
||||
Digit8 => "8",
|
||||
Digit9 => "9",
|
||||
Digit0 => "0",
|
||||
F1 => "F1",
|
||||
F2 => "F2",
|
||||
F3 => "F3",
|
||||
F4 => "F4",
|
||||
F5 => "F5",
|
||||
F6 => "F6",
|
||||
F7 => "F7",
|
||||
F8 => "F8",
|
||||
F9 => "F9",
|
||||
F10 => "F10",
|
||||
F11 => "F11",
|
||||
F12 => "F12",
|
||||
F1 => Word("F1"),
|
||||
F2 => Word("F2"),
|
||||
F3 => Word("F3"),
|
||||
F4 => Word("F4"),
|
||||
F5 => Word("F5"),
|
||||
F6 => Word("F6"),
|
||||
F7 => Word("F7"),
|
||||
F8 => Word("F8"),
|
||||
F9 => Word("F9"),
|
||||
F10 => Word("F10"),
|
||||
F11 => Word("F11"),
|
||||
F12 => Word("F12"),
|
||||
// On a Mac F13 is print-screen: the Mac key table `0x5bf320` maps keycode `0x69` to
|
||||
// `0x212`, `PRINTSCREEN`, and macOS has no PrintScreen keycode.
|
||||
#[cfg(target_os = "macos")]
|
||||
F13 => "PRINTSCREEN",
|
||||
F13 => Word("PRINTSCREEN"),
|
||||
// `IsValidBindingKeyString` accepts `F` + any digits (`0x846c04`) and the Mac table maps
|
||||
// keycode `0x6A` to `F16` (`0x30d`). Deviation: the Windows key table stops at `VK_F12`
|
||||
// and drops later F-keys; they are named here so a stored `F13`-`F24` can be pressed.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
F13 => "F13",
|
||||
F13 => Word("F13"),
|
||||
// On a Mac F14 and F15 are ScrollLock and Pause: keycodes `0x6B` and `0x71` map to
|
||||
// `0x210` and `0x211`, which the namer calls `UNKNOWN`, so they stay unbindable there.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
F14 => "F14",
|
||||
F14 => Word("F14"),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
F15 => "F15",
|
||||
F16 => "F16",
|
||||
F17 => "F17",
|
||||
F18 => "F18",
|
||||
F19 => "F19",
|
||||
F20 => "F20",
|
||||
F21 => "F21",
|
||||
F22 => "F22",
|
||||
F23 => "F23",
|
||||
F24 => "F24",
|
||||
Space => "SPACE",
|
||||
Tab => "TAB",
|
||||
Enter | NumpadEnter => "ENTER",
|
||||
Escape => "ESCAPE",
|
||||
Backspace => "BACKSPACE",
|
||||
Insert => "INSERT",
|
||||
Delete => "DELETE",
|
||||
Home => "HOME",
|
||||
End => "END",
|
||||
PageUp => "PAGEUP",
|
||||
PageDown => "PAGEDOWN",
|
||||
ArrowUp => "UP",
|
||||
ArrowDown => "DOWN",
|
||||
ArrowLeft => "LEFT",
|
||||
ArrowRight => "RIGHT",
|
||||
Numpad0 => "NUMPAD0",
|
||||
Numpad1 => "NUMPAD1",
|
||||
Numpad2 => "NUMPAD2",
|
||||
Numpad3 => "NUMPAD3",
|
||||
Numpad4 => "NUMPAD4",
|
||||
Numpad5 => "NUMPAD5",
|
||||
Numpad6 => "NUMPAD6",
|
||||
Numpad7 => "NUMPAD7",
|
||||
Numpad8 => "NUMPAD8",
|
||||
Numpad9 => "NUMPAD9",
|
||||
NumpadAdd => "NUMPADPLUS",
|
||||
NumpadSubtract => "NUMPADMINUS",
|
||||
NumpadDivide => "NUMPADDIVIDE",
|
||||
NumpadMultiply => "NUMPADMULTIPLY",
|
||||
NumpadDecimal => "NUMPADDECIMAL",
|
||||
F15 => Word("F15"),
|
||||
F16 => Word("F16"),
|
||||
F17 => Word("F17"),
|
||||
F18 => Word("F18"),
|
||||
F19 => Word("F19"),
|
||||
F20 => Word("F20"),
|
||||
F21 => Word("F21"),
|
||||
F22 => Word("F22"),
|
||||
F23 => Word("F23"),
|
||||
F24 => Word("F24"),
|
||||
Space => Word("SPACE"),
|
||||
Tab => Word("TAB"),
|
||||
Enter | NumpadEnter => Word("ENTER"),
|
||||
Escape => Word("ESCAPE"),
|
||||
Backspace => Word("BACKSPACE"),
|
||||
Insert => Word("INSERT"),
|
||||
Delete => Word("DELETE"),
|
||||
Home => Word("HOME"),
|
||||
End => Word("END"),
|
||||
PageUp => Word("PAGEUP"),
|
||||
PageDown => Word("PAGEDOWN"),
|
||||
ArrowUp => Word("UP"),
|
||||
ArrowDown => Word("DOWN"),
|
||||
ArrowLeft => Word("LEFT"),
|
||||
ArrowRight => Word("RIGHT"),
|
||||
Numpad0 => Word("NUMPAD0"),
|
||||
Numpad1 => Word("NUMPAD1"),
|
||||
Numpad2 => Word("NUMPAD2"),
|
||||
Numpad3 => Word("NUMPAD3"),
|
||||
Numpad4 => Word("NUMPAD4"),
|
||||
Numpad5 => Word("NUMPAD5"),
|
||||
Numpad6 => Word("NUMPAD6"),
|
||||
Numpad7 => Word("NUMPAD7"),
|
||||
Numpad8 => Word("NUMPAD8"),
|
||||
Numpad9 => Word("NUMPAD9"),
|
||||
NumpadAdd => Word("NUMPADPLUS"),
|
||||
NumpadSubtract => Word("NUMPADMINUS"),
|
||||
NumpadDivide => Word("NUMPADDIVIDE"),
|
||||
NumpadMultiply => Word("NUMPADMULTIPLY"),
|
||||
NumpadDecimal => Word("NUMPADDECIMAL"),
|
||||
// The Mac keypad's `=`: the namer's `0x30c`, and in `IsValidBindingKeyString`'s table.
|
||||
NumpadEqual => "NUMPADEQUALS",
|
||||
NumLock => "NUMLOCK",
|
||||
PrintScreen => "PRINTSCREEN",
|
||||
NumpadEqual => Word("NUMPADEQUALS"),
|
||||
NumLock => Word("NUMLOCK"),
|
||||
PrintScreen => Word("PRINTSCREEN"),
|
||||
// No ScrollLock or Pause: the namer (`0x4b66b0`) calls `0x210`/`0x211` `UNKNOWN`, and
|
||||
// `IsValidBindingKeyString` (`0x4b7890`) has neither name.
|
||||
CapsLock => "CAPSLOCK",
|
||||
Minus => "-",
|
||||
Equal => "=",
|
||||
BracketLeft => "[",
|
||||
BracketRight => "]",
|
||||
Backslash => "\\",
|
||||
Semicolon => ";",
|
||||
Quote => "'",
|
||||
Comma => ",",
|
||||
Period => ".",
|
||||
Slash => "/",
|
||||
Backquote => "`",
|
||||
CapsLock => Word("CAPSLOCK"),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fold key aliases that share one 1.12 token (`NumpadEnter` → `Enter`) so a chord parsed from
|
||||
/// `ENTER` matches either physical key.
|
||||
/// Fold the keys the reference gives one code (`NumpadEnter` → `Enter`, both `0x201`), so the
|
||||
/// pressed-key list and a latch treat them as one key.
|
||||
pub(crate) fn normalize_key(k: KeyCode) -> KeyCode {
|
||||
match k {
|
||||
KeyCode::NumpadEnter => KeyCode::Enter,
|
||||
// The Mac print-screen key; must agree with `key_token`, which names it for capture.
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyCode::F13 => KeyCode::PrintScreen,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
@@ -251,43 +237,15 @@ fn token_key(t: &str) -> Option<BindKey> {
|
||||
"MOUSEWHEELDOWN" => return Some(BindKey::WheelDown),
|
||||
_ => {}
|
||||
}
|
||||
// One character: what a layout names some key, which the namer writes as UTF-8 (`0x4b66b0`'s
|
||||
// `[0x21, 0xff]` arm through `0x41abb0`). [`key_token`] folds `a`-`z` up as the lookup does,
|
||||
// so a lowercase letter is no press's name.
|
||||
let mut chars = t.chars();
|
||||
if let (Some(c), None) = (chars.next(), chars.next()) {
|
||||
let pressable = !c.is_control() && !c.is_whitespace() && !c.is_ascii_lowercase();
|
||||
return pressable.then_some(BindKey::Key(KeyName::Char(c)));
|
||||
}
|
||||
let k = match t {
|
||||
"A" => KeyA,
|
||||
"B" => KeyB,
|
||||
"C" => KeyC,
|
||||
"D" => KeyD,
|
||||
"E" => KeyE,
|
||||
"F" => KeyF,
|
||||
"G" => KeyG,
|
||||
"H" => KeyH,
|
||||
"I" => KeyI,
|
||||
"J" => KeyJ,
|
||||
"K" => KeyK,
|
||||
"L" => KeyL,
|
||||
"M" => KeyM,
|
||||
"N" => KeyN,
|
||||
"O" => KeyO,
|
||||
"P" => KeyP,
|
||||
"Q" => KeyQ,
|
||||
"R" => KeyR,
|
||||
"S" => KeyS,
|
||||
"T" => KeyT,
|
||||
"U" => KeyU,
|
||||
"V" => KeyV,
|
||||
"W" => KeyW,
|
||||
"X" => KeyX,
|
||||
"Y" => KeyY,
|
||||
"Z" => KeyZ,
|
||||
"1" => Digit1,
|
||||
"2" => Digit2,
|
||||
"3" => Digit3,
|
||||
"4" => Digit4,
|
||||
"5" => Digit5,
|
||||
"6" => Digit6,
|
||||
"7" => Digit7,
|
||||
"8" => Digit8,
|
||||
"9" => Digit9,
|
||||
"0" => Digit0,
|
||||
"F1" => F1,
|
||||
"F2" => F2,
|
||||
"F3" => F3,
|
||||
@@ -346,26 +304,13 @@ fn token_key(t: &str) -> Option<BindKey> {
|
||||
"NUMLOCK" => NumLock,
|
||||
"PRINTSCREEN" => PrintScreen,
|
||||
"CAPSLOCK" => CapsLock,
|
||||
"-" => Minus,
|
||||
"=" => Equal,
|
||||
"[" => BracketLeft,
|
||||
"]" => BracketRight,
|
||||
"\\" => Backslash,
|
||||
";" => Semicolon,
|
||||
"'" => Quote,
|
||||
"," => Comma,
|
||||
"." => Period,
|
||||
"/" => Slash,
|
||||
"`" => Backquote,
|
||||
_ => return None,
|
||||
};
|
||||
// Fold aliases so a parsed chord equals a normalized press, and accept only a token the
|
||||
// namer produces on this platform (on a Mac, F13-F15 are other keys).
|
||||
let k = normalize_key(k);
|
||||
if key_token(k) != Some(t) {
|
||||
return None;
|
||||
// Only a name the namer gives on this platform: on a Mac, F13-F15 are other keys.
|
||||
match fixed_name(k) {
|
||||
Some(name @ KeyName::Word(w)) if w == t => Some(BindKey::Key(name)),
|
||||
_ => None,
|
||||
}
|
||||
Some(BindKey::Key(k))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -397,6 +342,7 @@ mod tests {
|
||||
"{command}: default '{default}' does not parse"
|
||||
);
|
||||
}
|
||||
let us = LayoutNames::default();
|
||||
for k in [
|
||||
KeyCode::KeyW,
|
||||
KeyCode::Digit0,
|
||||
@@ -431,9 +377,9 @@ mod tests {
|
||||
KeyCode::Slash,
|
||||
KeyCode::Backquote,
|
||||
] {
|
||||
let token = key_token(k).expect("named");
|
||||
let token = key_token(k, &us).expect("named").to_string();
|
||||
assert!(
|
||||
normalize_binding_key(token).is_some(),
|
||||
normalize_binding_key(&token).is_some(),
|
||||
"{k:?} names '{token}', which SetBinding refuses"
|
||||
);
|
||||
}
|
||||
@@ -447,8 +393,8 @@ mod tests {
|
||||
assert!(normalize_binding_key(token).is_some());
|
||||
}
|
||||
// The reference's namer calls these `UNKNOWN` (`0x210`/`0x211`).
|
||||
assert_eq!(key_token(KeyCode::ScrollLock), None);
|
||||
assert_eq!(key_token(KeyCode::Pause), None);
|
||||
assert_eq!(key_token(KeyCode::ScrollLock, &us), None);
|
||||
assert_eq!(key_token(KeyCode::Pause, &us), None);
|
||||
}
|
||||
|
||||
/// The validator's accept set is infinite, so this covers the tokens a real keyboard or mouse
|
||||
@@ -517,6 +463,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn the_codec_round_trips_every_keyboard_token() {
|
||||
let us = LayoutNames::default();
|
||||
let mut checked = 0;
|
||||
for k in [
|
||||
KeyCode::KeyW,
|
||||
@@ -530,38 +477,33 @@ mod tests {
|
||||
KeyCode::Backquote,
|
||||
KeyCode::Quote,
|
||||
] {
|
||||
let t = key_token(k).unwrap();
|
||||
let name = key_token(k, &us).unwrap();
|
||||
assert_eq!(
|
||||
token_key(t),
|
||||
Some(BindKey::Key(normalize_key(k))),
|
||||
"token {t}"
|
||||
token_key(&name.to_string()),
|
||||
Some(BindKey::Key(name)),
|
||||
"token {name}"
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, 10);
|
||||
assert_eq!(key_token(KeyCode::NumpadEnter), Some("ENTER"));
|
||||
assert_eq!(token_key("ENTER"), Some(BindKey::Key(KeyCode::Enter)));
|
||||
let enter = Some(KeyName::Word("ENTER"));
|
||||
assert_eq!(key_token(KeyCode::NumpadEnter, &us), enter);
|
||||
assert_eq!(key_token(KeyCode::Enter, &us), enter);
|
||||
assert_eq!(token_key("ENTER"), enter.map(BindKey::Key));
|
||||
}
|
||||
|
||||
/// macOS has no `PrintScreen`, so capture and dispatch must both read F13 as `PRINTSCREEN`.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn f13_is_print_screen_on_a_mac() {
|
||||
assert_eq!(
|
||||
key_token(KeyCode::F13),
|
||||
Some("PRINTSCREEN"),
|
||||
"the capture arm"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_key(KeyCode::F13),
|
||||
KeyCode::PrintScreen,
|
||||
"the dispatch arm"
|
||||
);
|
||||
let name = key_token(KeyCode::F13, &LayoutNames::default());
|
||||
assert_eq!(name, Some(KeyName::Word("PRINTSCREEN")));
|
||||
assert_eq!(
|
||||
token_key("PRINTSCREEN"),
|
||||
Some(BindKey::Key(normalize_key(KeyCode::F13))),
|
||||
name.map(BindKey::Key),
|
||||
"a chord parsed from the shipped default matches an F13 press"
|
||||
);
|
||||
assert_eq!(token_key("F13"), None, "no Mac key is F13");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -572,7 +514,7 @@ mod tests {
|
||||
alt: true,
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: BindKey::Key(KeyCode::F1)
|
||||
key: BindKey::Key(KeyName::Word("F1"))
|
||||
})
|
||||
);
|
||||
// `CTRL--` is Ctrl + the minus key.
|
||||
@@ -582,7 +524,7 @@ mod tests {
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
shift: false,
|
||||
key: BindKey::Key(KeyCode::Minus)
|
||||
key: BindKey::Key(KeyName::Char('-'))
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -610,8 +552,142 @@ mod tests {
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
shift: true,
|
||||
key: BindKey::Key(KeyCode::PageDown)
|
||||
key: BindKey::Key(KeyName::Word("PAGEDOWN"))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// The names a layout gives its keys, as `layout::record_layout_names` keeps them.
|
||||
fn layout(keys: &[(KeyCode, char)]) -> LayoutNames {
|
||||
let mut names = LayoutNames::default();
|
||||
for &(k, c) in keys {
|
||||
names.set(k, Some(LayoutName::Char(c)));
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
fn name(k: KeyCode, layout: &LayoutNames) -> Option<String> {
|
||||
key_token(k, layout).map(|n| n.to_string())
|
||||
}
|
||||
|
||||
/// 1.12 names a letter, digit or punctuation key what the active layout names it, its ASCII
|
||||
/// letters folded up as the lookup folds them, and every other key by the fixed table. AZERTY
|
||||
/// on Windows: `VK_Z` where a US W sits, `VK_M` on the semicolon key.
|
||||
#[test]
|
||||
fn a_layout_key_is_named_what_its_layout_names_it() {
|
||||
use KeyCode::*;
|
||||
let mut azerty = layout(&[
|
||||
(KeyW, 'Z'),
|
||||
(KeyZ, 'W'),
|
||||
(KeyQ, 'A'),
|
||||
(KeyA, 'Q'),
|
||||
(Semicolon, 'M'),
|
||||
(KeyM, ','),
|
||||
(Comma, ';'),
|
||||
(Period, ':'),
|
||||
(Slash, '!'),
|
||||
(Quote, 'ù'),
|
||||
(Backquote, '²'),
|
||||
(Minus, ')'),
|
||||
(BracketLeft, '^'),
|
||||
(BracketRight, '$'),
|
||||
(Backslash, '*'),
|
||||
(IntlBackslash, '<'),
|
||||
(NumpadComma, '.'),
|
||||
(Digit1, '1'),
|
||||
]);
|
||||
// Turkish Q's `VK_OEM_7` names its key `i`, which the lookup folds to `I`.
|
||||
azerty.set(KeyCode::KeyI, Some(LayoutName::Char('i')));
|
||||
for (k, expected) in [
|
||||
(KeyW, "Z"),
|
||||
(KeyZ, "W"),
|
||||
(KeyQ, "A"),
|
||||
(KeyA, "Q"),
|
||||
(Semicolon, "M"),
|
||||
(KeyM, ","),
|
||||
(Comma, ";"),
|
||||
(Period, ":"),
|
||||
(Slash, "!"),
|
||||
(Quote, "ù"),
|
||||
(Backquote, "²"),
|
||||
(Minus, ")"),
|
||||
(BracketLeft, "^"),
|
||||
(BracketRight, "$"),
|
||||
(Backslash, "*"),
|
||||
(IntlBackslash, "<"),
|
||||
(NumpadComma, "."),
|
||||
(Digit1, "1"),
|
||||
(KeyI, "I"),
|
||||
(F1, "F1"),
|
||||
(Numpad7, "NUMPAD7"),
|
||||
(Space, "SPACE"),
|
||||
] {
|
||||
assert_eq!(name(k, &azerty).as_deref(), Some(expected), "{k:?}");
|
||||
let token = key_token(k, &azerty).unwrap();
|
||||
assert_eq!(
|
||||
token_key(expected),
|
||||
Some(BindKey::Key(token)),
|
||||
"the stored '{expected}' is that key's chord"
|
||||
);
|
||||
}
|
||||
// A key its layout drops binds nothing (`0x42da49`).
|
||||
azerty.set(Backquote, Some(LayoutName::Dropped));
|
||||
assert_eq!(name(Backquote, &azerty), None);
|
||||
}
|
||||
|
||||
/// A US layout, reported or not, names every key as it always has.
|
||||
#[test]
|
||||
fn a_us_layout_names_its_keys_as_the_us_positions() {
|
||||
use KeyCode::*;
|
||||
let reported = layout(&[
|
||||
(KeyW, 'w'),
|
||||
(KeyZ, 'z'),
|
||||
(Quote, '\''),
|
||||
(Backquote, '`'),
|
||||
(Minus, '-'),
|
||||
(IntlBackslash, '\\'),
|
||||
]);
|
||||
let unreported = LayoutNames::default();
|
||||
for layout in [&reported, &unreported] {
|
||||
for (k, expected) in [
|
||||
(KeyW, "W"),
|
||||
(KeyZ, "Z"),
|
||||
(Quote, "'"),
|
||||
(Backquote, "`"),
|
||||
(Minus, "-"),
|
||||
(Digit1, "1"),
|
||||
] {
|
||||
assert_eq!(name(k, layout).as_deref(), Some(expected), "{k:?}");
|
||||
}
|
||||
}
|
||||
assert_eq!(name(IntlBackslash, &reported).as_deref(), Some("\\"));
|
||||
assert_eq!(
|
||||
name(IntlBackslash, &unreported),
|
||||
None,
|
||||
"an ISO key no layout has named yet"
|
||||
);
|
||||
}
|
||||
|
||||
/// Modifiers are prefixes, never part of the name: Shift+1 is `SHIFT-1`, not `!`.
|
||||
#[test]
|
||||
fn a_chord_names_its_key_unshifted() {
|
||||
let azerty = layout(&[(KeyCode::KeyW, 'z')]);
|
||||
let key = |k| BindKey::Key(key_token(k, &azerty).unwrap());
|
||||
assert_eq!(
|
||||
Chord::parse("SHIFT-Z").map(|c| (c.shift, c.key)),
|
||||
Some((true, key(KeyCode::KeyW)))
|
||||
);
|
||||
assert_eq!(
|
||||
Chord::parse("SHIFT-1").map(|c| (c.shift, c.key)),
|
||||
Some((true, key(KeyCode::Digit1)))
|
||||
);
|
||||
// Nothing presses a lowercase letter, a space or a control character.
|
||||
for t in ["w", " ", "\u{7}"] {
|
||||
assert_eq!(Chord::parse(t), None, "{t:?}");
|
||||
}
|
||||
assert_eq!(
|
||||
Chord::parse("ù").map(|c| c.key),
|
||||
Some(BindKey::Key(KeyName::Char('ù')))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
//! What the active keyboard layout names each letter, digit and punctuation key, which 1.12 binds
|
||||
//! by. The Windows client names a key from the virtual-key code the layout produced: the
|
||||
//! translator `0x42d800` keeps `VK_0`-`VK_9` as themselves (`0x42d81c`) and sends every other key
|
||||
//! outside its fixed table, `VK_A`-`VK_Z` and `VK_OEM_*` among them, to
|
||||
//! `MapVirtualKeyA(vk, MAPVK_VK_TO_CHAR)` (`0x42da39`), which returns `VK_A`-`VK_Z`'s own letter
|
||||
//! whatever the layout types there. The Mac client fills its letter and punctuation keys from the
|
||||
//! live layout, `KeyTranslate` (`0x8a3c3`) on the current `KCHR` (re-read when it changes,
|
||||
//! `0x8a345`), and fixes its digits by position (`0x5bf320`). Bevy's `KeyboardInput` carries the
|
||||
//! physical key and a `logical_key` with Shift applied, so each platform's name is read off the
|
||||
//! raw winit key messages here, before any `Update` reader.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bevy::input::keyboard::KeyCode;
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// A layout-named key's place on a US keyboard, with the US name there.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum Position {
|
||||
Letter(char),
|
||||
Digit(char),
|
||||
/// The ISO and JIS extra keys and the keypad comma have no US name.
|
||||
Punctuation(Option<char>),
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn us(self) -> Option<char> {
|
||||
match self {
|
||||
Position::Letter(c) | Position::Digit(c) => Some(c),
|
||||
Position::Punctuation(c) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys a layout names: the letters, the digits, the punctuation, the ISO and JIS extras and
|
||||
/// the keypad comma. Windows sends `VK_OEM_102`, `VK_ABNT_C2` and `VK_SEPARATOR` to the layout arm
|
||||
/// like any `VK_OEM_*` key, and the Mac table leaves the ISO key `0x0A` and the keypad comma
|
||||
/// `0x5F` to `KeyTranslate` (`-1` at `0x5bf320`). winit 0.30 cannot deliver those on a Mac, as on
|
||||
/// main: it reports `0x0A` as `Backquote` (whose scancode it gives back as `0x32`) and leaves
|
||||
/// `0x5E` and `0x5F` unidentified, so the `IntlBackslash`, `IntlRo` and `NumpadComma` arms never
|
||||
/// fire there.
|
||||
pub(crate) fn position(k: KeyCode) -> Option<Position> {
|
||||
use KeyCode::*;
|
||||
use Position::{Digit, Letter, Punctuation};
|
||||
Some(match k {
|
||||
KeyA => Letter('A'),
|
||||
KeyB => Letter('B'),
|
||||
KeyC => Letter('C'),
|
||||
KeyD => Letter('D'),
|
||||
KeyE => Letter('E'),
|
||||
KeyF => Letter('F'),
|
||||
KeyG => Letter('G'),
|
||||
KeyH => Letter('H'),
|
||||
KeyI => Letter('I'),
|
||||
KeyJ => Letter('J'),
|
||||
KeyK => Letter('K'),
|
||||
KeyL => Letter('L'),
|
||||
KeyM => Letter('M'),
|
||||
KeyN => Letter('N'),
|
||||
KeyO => Letter('O'),
|
||||
KeyP => Letter('P'),
|
||||
KeyQ => Letter('Q'),
|
||||
KeyR => Letter('R'),
|
||||
KeyS => Letter('S'),
|
||||
KeyT => Letter('T'),
|
||||
KeyU => Letter('U'),
|
||||
KeyV => Letter('V'),
|
||||
KeyW => Letter('W'),
|
||||
KeyX => Letter('X'),
|
||||
KeyY => Letter('Y'),
|
||||
KeyZ => Letter('Z'),
|
||||
Digit1 => Digit('1'),
|
||||
Digit2 => Digit('2'),
|
||||
Digit3 => Digit('3'),
|
||||
Digit4 => Digit('4'),
|
||||
Digit5 => Digit('5'),
|
||||
Digit6 => Digit('6'),
|
||||
Digit7 => Digit('7'),
|
||||
Digit8 => Digit('8'),
|
||||
Digit9 => Digit('9'),
|
||||
Digit0 => Digit('0'),
|
||||
Minus => Punctuation(Some('-')),
|
||||
Equal => Punctuation(Some('=')),
|
||||
BracketLeft => Punctuation(Some('[')),
|
||||
BracketRight => Punctuation(Some(']')),
|
||||
Backslash => Punctuation(Some('\\')),
|
||||
Semicolon => Punctuation(Some(';')),
|
||||
Quote => Punctuation(Some('\'')),
|
||||
Comma => Punctuation(Some(',')),
|
||||
Period => Punctuation(Some('.')),
|
||||
Slash => Punctuation(Some('/')),
|
||||
Backquote => Punctuation(Some('`')),
|
||||
IntlBackslash | IntlRo | IntlYen | NumpadComma => Punctuation(None),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A layout-named key's name.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum LayoutName {
|
||||
Char(char),
|
||||
/// None: `MapVirtualKeyA` returned 0 and the reference drops the key's message (`0x42da49`).
|
||||
Dropped,
|
||||
}
|
||||
|
||||
/// Each layout-named key's name under the active layout, as its platform last reported it: an
|
||||
/// entry changes with the next message of its key after a layout switch, so a press is named under
|
||||
/// the layout it was typed in. Empty where no winit feeds it (a headless run, a test).
|
||||
#[derive(Resource, Default, Debug)]
|
||||
pub(crate) struct LayoutNames(HashMap<KeyCode, LayoutName>);
|
||||
|
||||
impl LayoutNames {
|
||||
/// The name of `key` if a layout names it, `None` for a key the fixed table names. A key never
|
||||
/// reported (a headless run) keeps its US name.
|
||||
///
|
||||
/// Deviation: on the Mac and Linux a key its platform reports no character for keeps its US
|
||||
/// name too, where the Mac reference drops it (`0x8a4aa`), and a Linux dead key not yet pressed
|
||||
/// bare stays bindable.
|
||||
pub(crate) fn name(&self, key: KeyCode) -> Option<LayoutName> {
|
||||
let position = position(key)?;
|
||||
Some(match self.0.get(&key) {
|
||||
Some(name) => *name,
|
||||
None => position.us().map_or(LayoutName::Dropped, LayoutName::Char),
|
||||
})
|
||||
}
|
||||
|
||||
/// Set what the layout names `key`; `None` clears it back to the US name.
|
||||
pub(crate) fn set(&mut self, key: KeyCode, name: Option<LayoutName>) {
|
||||
match name {
|
||||
Some(name) => self.0.insert(key, name),
|
||||
None => self.0.remove(&key),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps [`LayoutNames`] current from the raw key messages (every shipped platform).
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_resource::<LayoutNames>();
|
||||
// A headless build has no winit to register it.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
app.add_message::<bevy::winit::RawWinitWindowEvent>()
|
||||
.add_systems(PreUpdate, record_layout_names);
|
||||
}
|
||||
|
||||
/// Names each key a raw key message reports, by its platform's rule. On the main thread, the
|
||||
/// window's: Windows reads that thread's layout, and macOS's input-source calls want it.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
fn record_layout_names(
|
||||
mut raw: MessageReader<bevy::winit::RawWinitWindowEvent>,
|
||||
mut names: ResMut<LayoutNames>,
|
||||
#[cfg(target_os = "linux")] mut mods: Local<LinuxMods>,
|
||||
_main_thread: bevy::ecs::system::NonSendMarker,
|
||||
) {
|
||||
use bevy::winit::converters::convert_physical_key_code;
|
||||
use winit::event::WindowEvent;
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut ascii_capable = None;
|
||||
for ev in raw.read() {
|
||||
let event = match &ev.event {
|
||||
WindowEvent::KeyboardInput { event, .. } => event,
|
||||
#[cfg(target_os = "linux")]
|
||||
WindowEvent::ModifiersChanged(m) => {
|
||||
mods.state = m.state();
|
||||
continue;
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
mods.key(event.physical_key, event.state.is_pressed());
|
||||
let key = convert_physical_key_code(event.physical_key);
|
||||
let Some(position) = position(key) else {
|
||||
continue;
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = position;
|
||||
names.set(key, windows::name(event.physical_key));
|
||||
}
|
||||
// The Mac and Linux keep their digits by position.
|
||||
#[cfg(target_os = "macos")]
|
||||
if !matches!(position, Position::Digit(_)) {
|
||||
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
|
||||
let capable = *ascii_capable.get_or_insert_with(mac::current_is_ascii_capable);
|
||||
let current = one_char(&event.key_without_modifiers());
|
||||
let name = mac_name(capable, current, || {
|
||||
mac::ascii_capable_char(event.physical_key)
|
||||
});
|
||||
names.set(key, name);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if !matches!(position, Position::Digit(_)) {
|
||||
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
|
||||
let unshifted = event.key_without_modifiers();
|
||||
if let Some(name) = linux_name(position, &unshifted, &event.logical_key, mods.bare()) {
|
||||
names.set(key, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The one printable character a winit key stands for.
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", test))]
|
||||
fn one_char(key: &winit::keyboard::Key) -> Option<char> {
|
||||
let winit::keyboard::Key::Character(s) = key else {
|
||||
return None;
|
||||
};
|
||||
let mut chars = s.chars();
|
||||
chars
|
||||
.next()
|
||||
.filter(|c| chars.next().is_none() && !c.is_control() && !c.is_whitespace())
|
||||
}
|
||||
|
||||
/// The reference's name for the virtual key `vk`, given `MapVirtualKeyExW(vk, MAPVK_VK_TO_CHAR)`;
|
||||
/// `None` for a key `0x42d800`'s fixed table names.
|
||||
///
|
||||
/// Deviation: a character above U+00FF names the key by itself. The reference's `MapVirtualKeyA`
|
||||
/// returns an ANSI code-page byte, which `0x42da4b` drops past `0xff` and the namer's UTF-8
|
||||
/// encoder (`0x41abb0`) otherwise reads as Latin-1, so such a key is mislabelled or dropped; named
|
||||
/// by what it types, it stays bindable and readable.
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn windows_name(vk: u32, to_char: u32) -> Option<LayoutName> {
|
||||
match vk {
|
||||
// `VK_A`-`VK_Z`, named by their letter, and `VK_0`-`VK_9`, kept as themselves.
|
||||
0x41..=0x5a | 0x30..=0x39 => Some(LayoutName::Char(char::from(vk as u8))),
|
||||
_ if fixed_vk(vk) => None,
|
||||
// The dynamic arm: `and eax,0xffff` (`0x42da42`) drops the dead-key bit, and 0 drops the
|
||||
// key (`0x42da49`). U+F000-U+F002 are a layout table's `WCH_NONE`, `WCH_DEAD` and
|
||||
// `WCH_LGTR` markers, no character, should the call ever return one.
|
||||
_ => Some(match char::from_u32(to_char & 0xffff) {
|
||||
Some(c)
|
||||
if !c.is_control()
|
||||
&& !c.is_whitespace()
|
||||
&& !('\u{f000}'..='\u{f002}').contains(&c) =>
|
||||
{
|
||||
LayoutName::Char(c)
|
||||
}
|
||||
_ => LayoutName::Dropped,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// `0x42d800`'s fixed table, read from its byte remap at `0x42dafc` and jump table at `0x42da60`:
|
||||
/// the keys it names itself, never by character. F1-F12 take their own arm (`0x42d800`).
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn fixed_vk(vk: u32) -> bool {
|
||||
matches!(
|
||||
vk,
|
||||
0x08 | 0x09
|
||||
| 0x0d
|
||||
| 0x10..=0x14
|
||||
| 0x1b
|
||||
| 0x20..=0x28
|
||||
| 0x2c..=0x2e
|
||||
| 0x60..=0x6b
|
||||
| 0x6d..=0x7b
|
||||
| 0x90
|
||||
| 0x91
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayout, MapVirtualKeyExW, MAPVK_VK_TO_CHAR, MAPVK_VSC_TO_VK_EX,
|
||||
};
|
||||
|
||||
/// The reference's own path for the key at `physical`: the virtual key the window thread's
|
||||
/// layout gives its scancode, then that key's name (`super::windows_name`).
|
||||
pub(super) fn name(physical: winit::keyboard::PhysicalKey) -> Option<super::LayoutName> {
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||
let scancode = physical.to_scancode()?;
|
||||
// SAFETY: plain Win32 calls with no pointers. `GetKeyboardLayout(0)` is the calling
|
||||
// thread's layout; the caller runs on the main thread, the window's, so it is the layout
|
||||
// the window's key messages were made under.
|
||||
let (vk, to_char) = unsafe {
|
||||
let hkl = GetKeyboardLayout(0);
|
||||
let vk = MapVirtualKeyExW(scancode, MAPVK_VSC_TO_VK_EX, hkl);
|
||||
(vk, MapVirtualKeyExW(vk, MAPVK_VK_TO_CHAR, hkl))
|
||||
};
|
||||
if vk == 0 {
|
||||
return None;
|
||||
}
|
||||
super::windows_name(vk, to_char)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Mac name of a letter or punctuation key: the layout's own unshifted character, as
|
||||
/// `KeyTranslate` gives the reference, or none.
|
||||
///
|
||||
/// Deviation: a layout that is not ASCII-capable (Hebrew, Greek, Russian, Arabic, Thai) names its
|
||||
/// keys through the ASCII-capable layout macOS pairs with it, as macOS names its own shortcuts. The
|
||||
/// reference reads a non-Roman `KCHR`'s byte as MacRoman (`0x8a40f`), which names no real key.
|
||||
///
|
||||
/// Deviation: a character above U+00FF (Czech `ě`, Polish `ł`, Turkish `ş`) names the key by
|
||||
/// itself. The reference reads `KeyTranslate`'s byte as MacRoman (`0x8a40f`) and keeps it only if
|
||||
/// it converts to Latin-1 (`CFStringGetBytes`, `0x8a46b`, stored at `0x8a482`), else leaves the
|
||||
/// slot `-1`, which `0x8a4aa` drops, so such a key is mislabelled or dropped; named by what it
|
||||
/// types, it stays bindable and readable.
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn mac_name(
|
||||
ascii_capable: bool,
|
||||
current: Option<char>,
|
||||
ascii_layout_char: impl FnOnce() -> Option<char>,
|
||||
) -> Option<LayoutName> {
|
||||
let c = if ascii_capable {
|
||||
current
|
||||
} else {
|
||||
ascii_layout_char()
|
||||
};
|
||||
c.map(LayoutName::Char)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac {
|
||||
use std::ffi::c_void;
|
||||
use std::os::raw::c_ulong;
|
||||
|
||||
type CFTypeRef = *const c_void;
|
||||
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CFRelease(cf: CFTypeRef);
|
||||
fn CFDataGetBytePtr(data: CFTypeRef) -> *const u8;
|
||||
fn CFBooleanGetValue(boolean: CFTypeRef) -> u8;
|
||||
}
|
||||
|
||||
#[link(name = "Carbon", kind = "framework")]
|
||||
extern "C" {
|
||||
static kTISPropertyInputSourceIsASCIICapable: CFTypeRef;
|
||||
static kTISPropertyUnicodeKeyLayoutData: CFTypeRef;
|
||||
fn TISCopyCurrentKeyboardLayoutInputSource() -> CFTypeRef;
|
||||
fn TISCopyCurrentASCIICapableKeyboardLayoutInputSource() -> CFTypeRef;
|
||||
fn TISGetInputSourceProperty(source: CFTypeRef, key: CFTypeRef) -> CFTypeRef;
|
||||
fn LMGetKbdType() -> u8;
|
||||
fn UCKeyTranslate(
|
||||
layout: *const u8,
|
||||
virtual_key_code: u16,
|
||||
key_action: u16,
|
||||
modifier_key_state: u32,
|
||||
keyboard_type: u32,
|
||||
key_translate_options: u32,
|
||||
dead_key_state: *mut u32,
|
||||
max_string_length: c_ulong,
|
||||
actual_string_length: *mut c_ulong,
|
||||
unicode_string: *mut u16,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
const K_UC_KEY_ACTION_DISPLAY: u16 = 3;
|
||||
const K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_MASK: u32 = 1;
|
||||
|
||||
/// Is the current keyboard layout one macOS types shortcuts with directly?
|
||||
pub(super) fn current_is_ascii_capable() -> bool {
|
||||
// SAFETY: the copied source is released here; the property is a borrowed CFBoolean.
|
||||
unsafe {
|
||||
let source = TISCopyCurrentKeyboardLayoutInputSource();
|
||||
if source.is_null() {
|
||||
return true;
|
||||
}
|
||||
let value = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsASCIICapable);
|
||||
let capable = value.is_null() || CFBooleanGetValue(value) != 0;
|
||||
CFRelease(source);
|
||||
capable
|
||||
}
|
||||
}
|
||||
|
||||
/// The unshifted character the ASCII-capable layout macOS pairs with the current input
|
||||
/// source makes on `physical`, dead keys off.
|
||||
pub(super) fn ascii_capable_char(physical: winit::keyboard::PhysicalKey) -> Option<char> {
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||
let keycode = u16::try_from(physical.to_scancode()?).ok()?;
|
||||
let mut buf = [0u16; 4];
|
||||
let mut len: c_ulong = 0;
|
||||
// SAFETY: the copied source owns the layout bytes and is released after the translate;
|
||||
// `buf` and `len` are valid for the length passed.
|
||||
let status = unsafe {
|
||||
let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
|
||||
if source.is_null() {
|
||||
return None;
|
||||
}
|
||||
let data = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData);
|
||||
let status = if data.is_null() {
|
||||
-1
|
||||
} else {
|
||||
let mut dead = 0u32;
|
||||
UCKeyTranslate(
|
||||
CFDataGetBytePtr(data),
|
||||
keycode,
|
||||
K_UC_KEY_ACTION_DISPLAY,
|
||||
0,
|
||||
u32::from(LMGetKbdType()),
|
||||
K_UC_KEY_TRANSLATE_NO_DEAD_KEYS_MASK,
|
||||
&mut dead,
|
||||
buf.len() as c_ulong,
|
||||
&mut len,
|
||||
buf.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
CFRelease(source);
|
||||
status
|
||||
};
|
||||
if status != 0 {
|
||||
return None;
|
||||
}
|
||||
let units = buf.get(..usize::try_from(len).ok()?)?;
|
||||
let mut chars = char::decode_utf16(units.iter().copied());
|
||||
let c = chars.next()?.ok()?;
|
||||
(chars.next().is_none() && !c.is_control() && !c.is_whitespace()).then_some(c)
|
||||
}
|
||||
}
|
||||
|
||||
/// The modifiers a Linux key press is made under, from the raw key stream. winit's
|
||||
/// `ModifiersState` has no AltGr, so the right Alt key is followed by its own messages: an AltGr
|
||||
/// press on a dead key types its AltGr level, never the key's own accent.
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[derive(Default)]
|
||||
struct LinuxMods {
|
||||
state: winit::keyboard::ModifiersState,
|
||||
alt_right: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
impl LinuxMods {
|
||||
/// Follow the right Alt key through a key message.
|
||||
fn key(&mut self, physical: winit::keyboard::PhysicalKey, pressed: bool) {
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
if physical == PhysicalKey::Code(KeyCode::AltRight) {
|
||||
self.alt_right = pressed;
|
||||
}
|
||||
}
|
||||
|
||||
/// No modifier held, AltGr included.
|
||||
fn bare(&self) -> bool {
|
||||
self.state.is_empty() && !self.alt_right
|
||||
}
|
||||
}
|
||||
|
||||
/// The Linux name of a letter or punctuation key, `None` to keep what the key had. No 1.12 client
|
||||
/// ran on Linux, so this is the closest consistent rule: a letter position takes an ASCII letter or
|
||||
/// ASCII punctuation the layout types there, and anything else there (another script's letter, an
|
||||
/// accented letter, a mark) keeps its US letter, as Windows keeps `VK_A`-`VK_Z` there on nearly
|
||||
/// every such layout; any other key takes its character. A dead keysym has no character
|
||||
/// (`key_without_modifiers` is `Unidentified`), so it takes the dead key's own character from a
|
||||
/// press with no modifier held, and otherwise keeps what it had.
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn linux_name(
|
||||
position: Position,
|
||||
unshifted: &winit::keyboard::Key,
|
||||
logical: &winit::keyboard::Key,
|
||||
bare: bool,
|
||||
) -> Option<Option<LayoutName>> {
|
||||
use winit::keyboard::Key;
|
||||
let c = match (unshifted, logical) {
|
||||
(Key::Unidentified(_), Key::Dead(Some(c))) if bare => Some(*c),
|
||||
(Key::Unidentified(_), _) => return None,
|
||||
_ => one_char(unshifted),
|
||||
};
|
||||
Some(c.map(|c| {
|
||||
LayoutName::Char(match position {
|
||||
Position::Letter(us) if !(c.is_ascii_alphabetic() || c.is_ascii_punctuation()) => us,
|
||||
_ => c,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A layout key's name under `names`, its ASCII letters folded up as the lookup folds them.
|
||||
fn named(names: &LayoutNames, k: KeyCode) -> Option<char> {
|
||||
match names.name(k) {
|
||||
Some(LayoutName::Char(c)) => Some(c.to_ascii_uppercase()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const VK_OEM_1: u32 = 0xba;
|
||||
const VK_OEM_COMMA: u32 = 0xbc;
|
||||
const VK_OEM_3: u32 = 0xc0;
|
||||
const VK_OEM_6: u32 = 0xdd;
|
||||
const VK_OEM_7: u32 = 0xde;
|
||||
|
||||
/// Windows: the reference's path on rows of the layouts' own tables (the key, its virtual
|
||||
/// key, its unshifted character), as `MapVirtualKeyExW` returns them.
|
||||
#[test]
|
||||
fn windows_names_a_key_by_its_virtual_key_as_the_reference_does() {
|
||||
let vk = |c: u8| u32::from(c);
|
||||
let rows: &[(&str, KeyCode, u32, u32, char)] = &[
|
||||
// Hebrew: the W key types `'` but is `VK_W`; Q types `/` and is `VK_Q`.
|
||||
("hebrew W", KeyCode::KeyW, vk(b'W'), '\''.into(), 'W'),
|
||||
("hebrew Q", KeyCode::KeyQ, vk(b'Q'), '/'.into(), 'Q'),
|
||||
("greek Q", KeyCode::KeyQ, vk(b'Q'), ';'.into(), 'Q'),
|
||||
("lithuanian Q", KeyCode::KeyQ, vk(b'Q'), 'ą'.into(), 'Q'),
|
||||
("lithuanian W", KeyCode::KeyW, vk(b'W'), 'ž'.into(), 'W'),
|
||||
("turkish ı", KeyCode::KeyI, vk(b'I'), 'ı'.into(), 'I'),
|
||||
// Turkish Q's `VK_OEM_7` types `i`: one name with `I`, as the lookup folds it.
|
||||
("turkish i", KeyCode::Quote, VK_OEM_7, 'i'.into(), 'I'),
|
||||
// Bulgarian: `VK_Q` on the Period key, `VK_OEM_COMMA` on Q.
|
||||
("bulgarian .", KeyCode::Period, vk(b'Q'), 'л'.into(), 'Q'),
|
||||
("bulgarian Q", KeyCode::KeyQ, VK_OEM_COMMA, ','.into(), ','),
|
||||
// Hungarian: `VK_0` on Backquote, `VK_OEM_3` `ö` on the 0 key.
|
||||
("hungarian `", KeyCode::Backquote, vk(b'0'), '0'.into(), '0'),
|
||||
("hungarian 0", KeyCode::Digit0, VK_OEM_3, 'ö'.into(), 'ö'),
|
||||
// Dvorak's Q position is `VK_OEM_7`, `'`.
|
||||
("dvorak Q", KeyCode::KeyQ, VK_OEM_7, '\''.into(), '\''),
|
||||
("us ;", KeyCode::Semicolon, VK_OEM_1, ';'.into(), ';'),
|
||||
// A dead key's bit (`0x80000000`) is masked off: French `^`.
|
||||
("french ^", KeyCode::BracketLeft, VK_OEM_6, 0x8000_005e, '^'),
|
||||
// Above U+00FF the key is named by what it types (the deviation).
|
||||
("russian ж", KeyCode::Semicolon, VK_OEM_1, 'ж'.into(), 'ж'),
|
||||
// The ISO key, German `VK_OEM_102`; the keypad comma, Brazilian ABNT's `VK_ABNT_C2`
|
||||
// and a `VK_SEPARATOR` keypad.
|
||||
("german <", KeyCode::IntlBackslash, 0xe2, '<'.into(), '<'),
|
||||
("abnt keypad .", KeyCode::NumpadComma, 0xc2, '.'.into(), '.'),
|
||||
("keypad ,", KeyCode::NumpadComma, 0x6c, ','.into(), ','),
|
||||
];
|
||||
let mut names = LayoutNames::default();
|
||||
for &(what, key, vk, to_char, expected) in rows {
|
||||
names.set(key, windows_name(vk, to_char));
|
||||
assert_eq!(named(&names, key), Some(expected), "{what}");
|
||||
}
|
||||
// `MapVirtualKeyA` returning 0 drops the key (`0x42da49`): Japanese 106's
|
||||
// `VK_OEM_AUTO` on Backquote.
|
||||
assert_eq!(windows_name(0xf3, 0), Some(LayoutName::Dropped));
|
||||
names.set(KeyCode::Backquote, windows_name(0xf3, 0));
|
||||
assert_eq!(names.name(KeyCode::Backquote), Some(LayoutName::Dropped));
|
||||
// A layout table's `WCH_NONE`/`WCH_DEAD`/`WCH_LGTR` marker is no character.
|
||||
for marker in 0xf000..=0xf002 {
|
||||
assert_eq!(
|
||||
windows_name(VK_OEM_1, marker),
|
||||
Some(LayoutName::Dropped),
|
||||
"{marker:#x}"
|
||||
);
|
||||
}
|
||||
// A key the fixed table names is no layout key's: F1, Space, Numpad 0.
|
||||
for vk in [0x70, 0x20, 0x60] {
|
||||
assert_eq!(windows_name(vk, 0), None, "{vk:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixed table is `0x42d800`'s, decoded from its remap bytes: the 60 entries less the
|
||||
/// digits and F1-F12, which take their own arms.
|
||||
#[test]
|
||||
fn the_fixed_virtual_keys_are_the_translators_table() {
|
||||
let fixed: Vec<u32> = (0..=0xffu32)
|
||||
.filter(|&vk| fixed_vk(vk) && !(0x70..=0x7b).contains(&vk))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
fixed,
|
||||
[
|
||||
0x08, 0x09, 0x0d, 0x10, 0x11, 0x12, 0x13, 0x14, 0x1b, 0x20, 0x21, 0x22, 0x23, 0x24,
|
||||
0x25, 0x26, 0x27, 0x28, 0x2c, 0x2d, 0x2e, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
||||
0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6d, 0x6e, 0x6f, 0x90, 0x91,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// macOS: a Latin layout's own character; a layout that is not ASCII-capable names through
|
||||
/// its ASCII-capable partner.
|
||||
#[test]
|
||||
fn a_mac_layout_that_is_not_ascii_capable_names_through_its_ascii_partner() {
|
||||
// Hebrew: W types `'`; its U.S. partner types `w` there.
|
||||
assert_eq!(
|
||||
mac_name(false, Some('\''), || Some('w')),
|
||||
Some(LayoutName::Char('w'))
|
||||
);
|
||||
// French: W types `z`, and the partner is never asked.
|
||||
assert_eq!(
|
||||
mac_name(true, Some('z'), || panic!(
|
||||
"an ASCII-capable layout names itself"
|
||||
)),
|
||||
Some(LayoutName::Char('z'))
|
||||
);
|
||||
// No character: the US name stands in (the fallback deviation).
|
||||
let mut names = LayoutNames::default();
|
||||
names.set(KeyCode::KeyW, mac_name(true, None, || None));
|
||||
assert_eq!(named(&names, KeyCode::KeyW), Some('W'));
|
||||
}
|
||||
|
||||
/// Linux: a letter position keeps its US letter unless the layout types an ASCII letter or
|
||||
/// ASCII punctuation there; any other key takes its character.
|
||||
#[test]
|
||||
fn linux_names_a_letter_position_by_its_ascii_character_or_its_us_letter() {
|
||||
use winit::keyboard::{Key, NativeKey};
|
||||
let ch = |c: &str| Key::Character(c.into());
|
||||
for (key, typed, expected) in [
|
||||
(KeyCode::KeyW, "z", 'z'),
|
||||
(KeyCode::KeyM, ",", ','),
|
||||
(KeyCode::KeyQ, "'", '\''),
|
||||
(KeyCode::KeyQ, "ą", 'Q'),
|
||||
(KeyCode::KeyI, "ı", 'I'),
|
||||
(KeyCode::KeyW, "ц", 'W'),
|
||||
// Thai's tone mark on H, INSCRIPT's virama on D.
|
||||
(KeyCode::KeyH, "\u{e48}", 'H'),
|
||||
(KeyCode::KeyD, "\u{94d}", 'D'),
|
||||
(KeyCode::Semicolon, "ж", 'ж'),
|
||||
(KeyCode::Quote, "ù", 'ù'),
|
||||
] {
|
||||
let got = linux_name(position(key).unwrap(), &ch(typed), &ch(typed), true);
|
||||
assert_eq!(
|
||||
got,
|
||||
Some(Some(LayoutName::Char(expected))),
|
||||
"{key:?} typing {typed}"
|
||||
);
|
||||
}
|
||||
// A dead keysym: its character from a bare press, kept through a modified one.
|
||||
let none = Key::Unidentified(NativeKey::Unidentified);
|
||||
let bracket = position(KeyCode::BracketLeft).unwrap();
|
||||
assert_eq!(
|
||||
linux_name(bracket, &none, &Key::Dead(Some('^')), true),
|
||||
Some(Some(LayoutName::Char('^')))
|
||||
);
|
||||
assert_eq!(
|
||||
linux_name(bracket, &none, &Key::Dead(Some('¨')), false),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layout_switch_renames_a_key_at_its_next_message() {
|
||||
let mut names = LayoutNames::default();
|
||||
names.set(KeyCode::KeyW, Some(LayoutName::Char('z')));
|
||||
assert_eq!(named(&names, KeyCode::KeyW), Some('Z'));
|
||||
names.set(KeyCode::KeyW, Some(LayoutName::Char('w')));
|
||||
assert_eq!(
|
||||
named(&names, KeyCode::KeyW),
|
||||
Some('W'),
|
||||
"back on a US layout"
|
||||
);
|
||||
// A layout that makes no character there falls back to the US name.
|
||||
names.set(KeyCode::KeyQ, Some(LayoutName::Char('a')));
|
||||
names.set(KeyCode::KeyQ, None);
|
||||
assert_eq!(named(&names, KeyCode::KeyQ), Some('Q'));
|
||||
// Digits and punctuation name themselves until a layout says otherwise; an ISO key no
|
||||
// layout reported has no name.
|
||||
assert_eq!(named(&names, KeyCode::Digit1), Some('1'));
|
||||
assert_eq!(named(&names, KeyCode::Quote), Some('\''));
|
||||
for key in [KeyCode::IntlBackslash, KeyCode::NumpadComma] {
|
||||
assert_eq!(names.name(key), Some(LayoutName::Dropped), "{key:?}");
|
||||
}
|
||||
assert_eq!(names.name(KeyCode::F1), None, "the fixed table's");
|
||||
}
|
||||
|
||||
/// Linux: AltGr counts as a modifier for a dead key's bare press, though winit's
|
||||
/// `ModifiersState` has no AltGr; AltGr on a dead-key position keeps the key's own accent.
|
||||
#[test]
|
||||
fn linux_altgr_is_no_bare_press() {
|
||||
use winit::keyboard::{Key, KeyCode, ModifiersState, NativeKey, PhysicalKey};
|
||||
let none = Key::Unidentified(NativeKey::Unidentified);
|
||||
let bracket = position(bevy::input::keyboard::KeyCode::BracketLeft).unwrap();
|
||||
let mut mods = LinuxMods::default();
|
||||
assert!(mods.bare());
|
||||
mods.key(PhysicalKey::Code(KeyCode::AltRight), true);
|
||||
assert!(!mods.bare(), "AltGr held");
|
||||
// AltGr on the French `^` key types `~`: the key keeps its `^`.
|
||||
assert_eq!(
|
||||
linux_name(bracket, &none, &Key::Dead(Some('~')), mods.bare()),
|
||||
None
|
||||
);
|
||||
mods.key(PhysicalKey::Code(KeyCode::AltRight), false);
|
||||
assert!(mods.bare(), "AltGr released");
|
||||
mods.key(PhysicalKey::Code(KeyCode::KeyQ), true);
|
||||
assert!(mods.bare(), "another key is no modifier");
|
||||
mods.state = ModifiersState::SHIFT;
|
||||
assert!(!mods.bare(), "Shift held");
|
||||
}
|
||||
}
|
||||
@@ -340,8 +340,9 @@ fn create_input(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
catalog: Option<Res<CharCreate>>,
|
||||
mut sel: ResMut<CreateSelection>,
|
||||
// The host pasteboard and the window handle its Wayland backend needs.
|
||||
mut clipboard: NonSendMut<HostClipboard>,
|
||||
// The host pasteboard and the window handle its Wayland backend needs, and the layout that
|
||||
// names the clipboard chords' letters.
|
||||
(mut clipboard, layout): (NonSendMut<HostClipboard>, Res<crate::bindings::LayoutNames>),
|
||||
raw_handle: Query<&bevy::window::RawHandleWrapper, With<bevy::window::PrimaryWindow>>,
|
||||
time: Res<Time>,
|
||||
mut preview: ResMut<GluePreview>,
|
||||
@@ -421,6 +422,7 @@ fn create_input(
|
||||
&mut sel.name,
|
||||
ev,
|
||||
mods,
|
||||
&layout,
|
||||
&mut clipboard,
|
||||
wl,
|
||||
textinput::CharFilter::Letters,
|
||||
|
||||
@@ -92,7 +92,8 @@ pub(super) fn drive_delete_dialog(
|
||||
Option<&mut Text>,
|
||||
&mut Visibility,
|
||||
)>,
|
||||
mut clipboard: NonSendMut<HostClipboard>,
|
||||
// The pasteboard, and the layout that names the clipboard chords' letters.
|
||||
(mut clipboard, layout): (NonSendMut<HostClipboard>, Res<crate::bindings::LayoutNames>),
|
||||
// The window drives the glue scale; its raw handle carries the Wayland clipboard's display.
|
||||
window: Query<
|
||||
(&Window, Option<&bevy::window::RawHandleWrapper>),
|
||||
@@ -139,6 +140,7 @@ pub(super) fn drive_delete_dialog(
|
||||
&mut dialog.typed,
|
||||
ev,
|
||||
mods,
|
||||
&layout,
|
||||
&mut clipboard,
|
||||
wl,
|
||||
textinput::CharFilter::Any,
|
||||
|
||||
@@ -651,7 +651,8 @@ fn login_input(
|
||||
clicks: Res<crate::glue::GlueClicks>,
|
||||
mut keyboard: MessageReader<KeyboardInput>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut clipboard: NonSendMut<HostClipboard>,
|
||||
// The pasteboard, and the layout that names the clipboard chords' letters.
|
||||
(mut clipboard, layout): (NonSendMut<HostClipboard>, Res<crate::bindings::LayoutNames>),
|
||||
raw_handle: Query<&bevy::window::RawHandleWrapper, With<bevy::window::PrimaryWindow>>,
|
||||
mut form: ResMut<LoginForm>,
|
||||
mut attempt: Attempt,
|
||||
@@ -742,6 +743,7 @@ fn login_input(
|
||||
&mut dialog.edit,
|
||||
ev,
|
||||
mods,
|
||||
&layout,
|
||||
&mut clipboard,
|
||||
wl,
|
||||
textinput::CharFilter::Any,
|
||||
@@ -755,6 +757,7 @@ fn login_input(
|
||||
form.focused(),
|
||||
ev,
|
||||
mods,
|
||||
&layout,
|
||||
&mut clipboard,
|
||||
wl,
|
||||
textinput::CharFilter::Any,
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
use benilla_ui::script::{EditAction, EditUnit};
|
||||
use bevy::input::keyboard::KeyCode;
|
||||
|
||||
use crate::bindings::chord::KeyName;
|
||||
|
||||
/// The modifier snapshot a chord is read against; `sup` is Cmd on macOS, the OS key elsewhere.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct Mods {
|
||||
@@ -29,8 +31,24 @@ pub(crate) enum Chord {
|
||||
Paste,
|
||||
}
|
||||
|
||||
/// What `key` under `m` means; `None` falls through to character input.
|
||||
pub(crate) fn chord(key: KeyCode, m: Mods, mac: bool) -> Option<Chord> {
|
||||
/// What `key`, which the layout names `name`, means under `m`; `None` falls through to character
|
||||
/// input.
|
||||
pub(crate) fn chord(key: KeyCode, name: Option<KeyName>, m: Mods, mac: bool) -> Option<Chord> {
|
||||
// Ctrl (Cmd on a Mac) with the keys the layout names A, C, X and V: the reference's handler
|
||||
// switches on the key's code (`0x77b1fb`), the name its bindings use, so on AZERTY select-all
|
||||
// is the key labelled A. On the PC `!m.alt` excludes AltGr, which arrives as Ctrl+Alt and types
|
||||
// letters on European layouts (Polish AltGr+A is `ą`); must agree with the char-input branch
|
||||
// in `input.rs`.
|
||||
let held = if mac { m.sup } else { m.ctrl && !m.alt };
|
||||
if let (true, Some(KeyName::Char(letter))) = (held, name) {
|
||||
match letter {
|
||||
'A' => return Some(Chord::Edit(EditAction::SelectAll)),
|
||||
'C' => return Some(Chord::Copy),
|
||||
'X' => return Some(Chord::Cut),
|
||||
'V' => return Some(Chord::Paste),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if mac {
|
||||
chord_mac(key, m)
|
||||
} else {
|
||||
@@ -94,10 +112,6 @@ fn chord_mac(key: KeyCode, m: Mods) -> Option<Chord> {
|
||||
del(Char, false)
|
||||
}
|
||||
}
|
||||
KeyCode::KeyA if m.sup => Some(Chord::Edit(EditAction::SelectAll)),
|
||||
KeyCode::KeyC if m.sup => Some(Chord::Copy),
|
||||
KeyCode::KeyX if m.sup => Some(Chord::Cut),
|
||||
KeyCode::KeyV if m.sup => Some(Chord::Paste),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -152,12 +166,6 @@ fn chord_pc(key: KeyCode, m: Mods) -> Option<Chord> {
|
||||
// The reference's other CUA mirrors: Ctrl+Insert copies, Shift+Insert pastes.
|
||||
KeyCode::Insert if m.ctrl => Some(Chord::Copy),
|
||||
KeyCode::Insert if m.shift => Some(Chord::Paste),
|
||||
// `!m.alt` excludes AltGr, which arrives as Ctrl+Alt and types letters on European
|
||||
// layouts (Polish AltGr+A is `ą`); must agree with the char-input branch in `input.rs`.
|
||||
KeyCode::KeyA if m.ctrl && !m.alt => Some(Chord::Edit(EditAction::SelectAll)),
|
||||
KeyCode::KeyC if m.ctrl && !m.alt => Some(Chord::Copy),
|
||||
KeyCode::KeyX if m.ctrl && !m.alt => Some(Chord::Cut),
|
||||
KeyCode::KeyV if m.ctrl && !m.alt => Some(Chord::Paste),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -180,6 +188,11 @@ mod tests {
|
||||
const ALT: Mods = Mods { alt: true, ..NONE };
|
||||
const SUP: Mods = Mods { sup: true, ..NONE };
|
||||
|
||||
/// A key's name on a US layout.
|
||||
fn us(key: KeyCode) -> Option<KeyName> {
|
||||
crate::bindings::chord::key_token(key, &Default::default())
|
||||
}
|
||||
|
||||
fn edit(c: Option<Chord>) -> EditAction {
|
||||
match c {
|
||||
Some(Chord::Edit(a)) => a,
|
||||
@@ -192,7 +205,12 @@ mod tests {
|
||||
use EditAction::*;
|
||||
use EditUnit::*;
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowLeft, NONE, true)),
|
||||
edit(chord(
|
||||
KeyCode::ArrowLeft,
|
||||
us(KeyCode::ArrowLeft),
|
||||
NONE,
|
||||
true
|
||||
)),
|
||||
Move {
|
||||
unit: Char,
|
||||
back: true,
|
||||
@@ -200,7 +218,12 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowRight, ALT, true)),
|
||||
edit(chord(
|
||||
KeyCode::ArrowRight,
|
||||
us(KeyCode::ArrowRight),
|
||||
ALT,
|
||||
true
|
||||
)),
|
||||
Move {
|
||||
unit: Word,
|
||||
back: false,
|
||||
@@ -208,7 +231,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowLeft, SUP, true)),
|
||||
edit(chord(KeyCode::ArrowLeft, us(KeyCode::ArrowLeft), SUP, true)),
|
||||
Move {
|
||||
unit: Edge,
|
||||
back: true,
|
||||
@@ -218,6 +241,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
edit(chord(
|
||||
KeyCode::ArrowRight,
|
||||
us(KeyCode::ArrowRight),
|
||||
Mods { shift: true, ..ALT },
|
||||
true
|
||||
)),
|
||||
@@ -227,10 +251,21 @@ mod tests {
|
||||
extend: true
|
||||
}
|
||||
);
|
||||
assert_eq!(edit(chord(KeyCode::ArrowUp, NONE, true)), HistoryPrev);
|
||||
assert_eq!(edit(chord(KeyCode::ArrowDown, NONE, true)), HistoryNext);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowUp, SHIFT, true)),
|
||||
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), NONE, true)),
|
||||
HistoryPrev
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(
|
||||
KeyCode::ArrowDown,
|
||||
us(KeyCode::ArrowDown),
|
||||
NONE,
|
||||
true
|
||||
)),
|
||||
HistoryNext
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), SHIFT, true)),
|
||||
Move {
|
||||
unit: Edge,
|
||||
back: true,
|
||||
@@ -238,7 +273,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowDown, SUP, true)),
|
||||
edit(chord(KeyCode::ArrowDown, us(KeyCode::ArrowDown), SUP, true)),
|
||||
Move {
|
||||
unit: Edge,
|
||||
back: false,
|
||||
@@ -246,32 +281,44 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::Backspace, SUP, true)),
|
||||
edit(chord(KeyCode::Backspace, us(KeyCode::Backspace), SUP, true)),
|
||||
Delete {
|
||||
unit: Edge,
|
||||
back: true
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::Backspace, ALT, true)),
|
||||
edit(chord(KeyCode::Backspace, us(KeyCode::Backspace), ALT, true)),
|
||||
Delete {
|
||||
unit: Word,
|
||||
back: true
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::Delete, ALT, true)),
|
||||
edit(chord(KeyCode::Delete, us(KeyCode::Delete), ALT, true)),
|
||||
Delete {
|
||||
unit: Word,
|
||||
back: false
|
||||
}
|
||||
);
|
||||
assert_eq!(edit(chord(KeyCode::KeyA, SUP, true)), SelectAll);
|
||||
assert_eq!(chord(KeyCode::KeyC, SUP, true), Some(Chord::Copy));
|
||||
assert_eq!(chord(KeyCode::KeyX, SUP, true), Some(Chord::Cut));
|
||||
assert_eq!(chord(KeyCode::KeyV, SUP, true), Some(Chord::Paste));
|
||||
assert_eq!(chord(KeyCode::KeyA, CTRL, true), None);
|
||||
assert_eq!(chord(KeyCode::KeyA, NONE, true), None);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::KeyA, us(KeyCode::KeyA), SUP, true)),
|
||||
SelectAll
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyC, us(KeyCode::KeyC), SUP, true),
|
||||
Some(Chord::Copy)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyX, us(KeyCode::KeyX), SUP, true),
|
||||
Some(Chord::Cut)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyV, us(KeyCode::KeyV), SUP, true),
|
||||
Some(Chord::Paste)
|
||||
);
|
||||
assert_eq!(chord(KeyCode::KeyA, us(KeyCode::KeyA), CTRL, true), None);
|
||||
assert_eq!(chord(KeyCode::KeyA, us(KeyCode::KeyA), NONE, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,7 +326,12 @@ mod tests {
|
||||
use EditAction::*;
|
||||
use EditUnit::*;
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowLeft, CTRL, false)),
|
||||
edit(chord(
|
||||
KeyCode::ArrowLeft,
|
||||
us(KeyCode::ArrowLeft),
|
||||
CTRL,
|
||||
false
|
||||
)),
|
||||
Move {
|
||||
unit: Word,
|
||||
back: true,
|
||||
@@ -287,17 +339,28 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::ArrowRight, SHIFT, false)),
|
||||
edit(chord(
|
||||
KeyCode::ArrowRight,
|
||||
us(KeyCode::ArrowRight),
|
||||
SHIFT,
|
||||
false
|
||||
)),
|
||||
Move {
|
||||
unit: Char,
|
||||
back: false,
|
||||
extend: true
|
||||
}
|
||||
);
|
||||
assert_eq!(edit(chord(KeyCode::ArrowUp, NONE, false)), HistoryPrev);
|
||||
assert_eq!(chord(KeyCode::ArrowUp, SHIFT, false), None);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::End, SHIFT, false)),
|
||||
edit(chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), NONE, false)),
|
||||
HistoryPrev
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::ArrowUp, us(KeyCode::ArrowUp), SHIFT, false),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::End, us(KeyCode::End), SHIFT, false)),
|
||||
Move {
|
||||
unit: Edge,
|
||||
back: false,
|
||||
@@ -305,26 +368,49 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::Backspace, CTRL, false)),
|
||||
edit(chord(
|
||||
KeyCode::Backspace,
|
||||
us(KeyCode::Backspace),
|
||||
CTRL,
|
||||
false
|
||||
)),
|
||||
Delete {
|
||||
unit: Word,
|
||||
back: true
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::Delete, CTRL, false)),
|
||||
edit(chord(KeyCode::Delete, us(KeyCode::Delete), CTRL, false)),
|
||||
Delete {
|
||||
unit: Word,
|
||||
back: false
|
||||
}
|
||||
);
|
||||
assert_eq!(edit(chord(KeyCode::KeyA, CTRL, false)), SelectAll);
|
||||
assert_eq!(chord(KeyCode::KeyC, CTRL, false), Some(Chord::Copy));
|
||||
assert_eq!(chord(KeyCode::KeyV, CTRL, false), Some(Chord::Paste));
|
||||
assert_eq!(chord(KeyCode::Insert, CTRL, false), Some(Chord::Copy));
|
||||
assert_eq!(chord(KeyCode::Insert, SHIFT, false), Some(Chord::Paste));
|
||||
assert_eq!(chord(KeyCode::Delete, SHIFT, false), Some(Chord::Cut));
|
||||
assert_eq!(chord(KeyCode::KeyA, SUP, false), None);
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::KeyA, us(KeyCode::KeyA), CTRL, false)),
|
||||
SelectAll
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyC, us(KeyCode::KeyC), CTRL, false),
|
||||
Some(Chord::Copy)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyV, us(KeyCode::KeyV), CTRL, false),
|
||||
Some(Chord::Paste)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::Insert, us(KeyCode::Insert), CTRL, false),
|
||||
Some(Chord::Copy)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::Insert, us(KeyCode::Insert), SHIFT, false),
|
||||
Some(Chord::Paste)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::Delete, us(KeyCode::Delete), SHIFT, false),
|
||||
Some(Chord::Cut)
|
||||
);
|
||||
assert_eq!(chord(KeyCode::KeyA, us(KeyCode::KeyA), SUP, false), None);
|
||||
}
|
||||
|
||||
/// AltGr arrives as Ctrl+Alt and types letters on European layouts (Polish `ą`, `ć`, `ź`).
|
||||
@@ -337,17 +423,48 @@ mod tests {
|
||||
};
|
||||
for key in [KeyCode::KeyA, KeyCode::KeyC, KeyCode::KeyX, KeyCode::KeyV] {
|
||||
assert_eq!(
|
||||
chord(key, ALTGR, false),
|
||||
chord(key, us(key), ALTGR, false),
|
||||
None,
|
||||
"AltGr+{key:?} must reach character input, not act as a clipboard chord"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::KeyA, CTRL, false)),
|
||||
edit(chord(KeyCode::KeyA, us(KeyCode::KeyA), CTRL, false)),
|
||||
EditAction::SelectAll
|
||||
);
|
||||
assert_eq!(chord(KeyCode::KeyC, CTRL, false), Some(Chord::Copy));
|
||||
assert_eq!(chord(KeyCode::KeyX, CTRL, false), Some(Chord::Cut));
|
||||
assert_eq!(chord(KeyCode::KeyV, CTRL, false), Some(Chord::Paste));
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyC, us(KeyCode::KeyC), CTRL, false),
|
||||
Some(Chord::Copy)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyX, us(KeyCode::KeyX), CTRL, false),
|
||||
Some(Chord::Cut)
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyV, us(KeyCode::KeyV), CTRL, false),
|
||||
Some(Chord::Paste)
|
||||
);
|
||||
}
|
||||
|
||||
/// Select-all, copy, cut and paste follow the layout's names, as the reference's handler
|
||||
/// switches on the key's code (`0x77b1fb`): on AZERTY they sit on the keys labelled A, C, X
|
||||
/// and V, wherever those are.
|
||||
#[test]
|
||||
fn the_clipboard_chords_sit_on_the_keys_the_layout_names() {
|
||||
let azerty_a = Some(KeyName::Char('A'));
|
||||
let azerty_q = Some(KeyName::Char('Q'));
|
||||
for mac in [false, true] {
|
||||
let held = if mac { SUP } else { CTRL };
|
||||
assert_eq!(
|
||||
edit(chord(KeyCode::KeyQ, azerty_a, held, mac)),
|
||||
EditAction::SelectAll,
|
||||
"the key labelled A (mac: {mac})"
|
||||
);
|
||||
assert_eq!(
|
||||
chord(KeyCode::KeyA, azerty_q, held, mac),
|
||||
None,
|
||||
"the key labelled Q (mac: {mac})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +74,13 @@ pub(crate) enum FieldKey {
|
||||
Passthrough,
|
||||
}
|
||||
|
||||
/// Feed one key press to `field`. `wl_display` comes from [`wayland_display`], `None` off Wayland.
|
||||
/// Feed one key press to `field`, its letters named by `layout`. `wl_display` comes from
|
||||
/// [`wayland_display`], `None` off Wayland.
|
||||
pub(crate) fn feed_key(
|
||||
field: &mut EditBoxState,
|
||||
ev: &KeyboardInput,
|
||||
mods: Mods,
|
||||
layout: &crate::bindings::LayoutNames,
|
||||
clipboard: &mut HostClipboard,
|
||||
wl_display: Option<*mut c_void>,
|
||||
filter: CharFilter,
|
||||
@@ -93,7 +95,8 @@ pub(crate) fn feed_key(
|
||||
) {
|
||||
return FieldKey::Passthrough;
|
||||
}
|
||||
if let Some(chord) = chord(ev.key_code, mods, cfg!(target_os = "macos")) {
|
||||
let name = crate::bindings::chord::key_token(ev.key_code, layout);
|
||||
if let Some(chord) = chord(ev.key_code, name, mods, cfg!(target_os = "macos")) {
|
||||
match chord {
|
||||
Chord::Edit(action) => {
|
||||
field.apply(action);
|
||||
|
||||
@@ -97,8 +97,8 @@ fn the_host_has_a_reference_name_for_the_keys_it_now_delivers() {
|
||||
(KeyCode::F1, "F1"),
|
||||
] {
|
||||
assert_eq!(
|
||||
key_token(code),
|
||||
Some(name),
|
||||
key_token(code, &Default::default()).map(|n| n.to_string()),
|
||||
Some(name.to_string()),
|
||||
"{code:?} must reach a keyboard frame under the reference's own name"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ pub(super) fn feed_ui_input(
|
||||
ResMut<UiKeyboardCapture>,
|
||||
NonSendMut<HostClipboard>,
|
||||
),
|
||||
// The characters the active layout makes, which name a key for a keyboard frame.
|
||||
layout: Res<crate::bindings::LayoutNames>,
|
||||
// The uiScale dial folded into the seam scale.
|
||||
ui_scale: Res<super::UiScaleCvar>,
|
||||
) {
|
||||
@@ -232,8 +234,11 @@ pub(super) fn feed_ui_input(
|
||||
KeyCode::Tab => Some("TAB"),
|
||||
_ => None,
|
||||
};
|
||||
// What the layout names the key: the clipboard chords' letters, and a keyboard frame's
|
||||
// `arg1` below.
|
||||
let token = crate::bindings::chord::key_token(ev.key_code, &layout);
|
||||
// Dispatched unconditionally: unfocused, they fall through to the camera and turn keys.
|
||||
let chord = keymap::chord(ev.key_code, mods, mac);
|
||||
let chord = keymap::chord(ev.key_code, token, mods, mac);
|
||||
// A keyboard frame gets these by name before their chord runs (a dialog needs BACKSPACE);
|
||||
// `frame_key_input` declines at a focused box, so no frame steals its editing keys. `true`
|
||||
// suppresses the chord and the key's binding (the reference's existence gate, `0x76b7d0`).
|
||||
@@ -258,14 +263,15 @@ pub(super) fn feed_ui_input(
|
||||
}
|
||||
// Every other key reaches a keyboard frame by name too: the reference's key-down walk
|
||||
// takes its `arg1` from the table the binding chord uses (`0x4b66b0`), `chord::key_token`
|
||||
// here, and the gate is existence, not handling: a shown keyboard frame with an
|
||||
// `OnKeyDown` swallows the key whatever its script does (`0x76b7d0`, `0x76ba25`).
|
||||
// under the active layout here, so the Key Bindings window stores the name the key's
|
||||
// press dispatches by. The gate is existence, not handling: a shown keyboard frame with
|
||||
// an `OnKeyDown` swallows the key whatever its script does (`0x76b7d0`, `0x76ba25`).
|
||||
// Consumption suppresses only the key's binding: `OnChar` is a separate dispatcher
|
||||
// (`0x765df0`), so the stack-split spinner still gets a digit its `OnKeyDown` ate, and it
|
||||
// is not a focus change, so it releases nothing held.
|
||||
else if named.is_none() {
|
||||
if let Some(token) = crate::bindings::chord::key_token(ev.key_code) {
|
||||
if script.frame_key_input(token) {
|
||||
if let Some(token) = token {
|
||||
if script.frame_key_input(&token.to_string()) {
|
||||
capture.consumed.push(ev.key_code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ enum Verdict {
|
||||
}
|
||||
use Verdict::{Open, Plumbing, Swallowed};
|
||||
|
||||
/// Every `bevy::input::…` / `bevy::window::…` leaf the workspace names, plus the prelude input
|
||||
/// items, each with its verdict.
|
||||
/// Every `bevy::input::…` / `bevy::window::…` / `bevy::winit::…` leaf the workspace names, plus
|
||||
/// the prelude input items, each with its verdict.
|
||||
const VERDICTS: &[(&str, Verdict)] = &[
|
||||
// ── The channels the cover takes ────────────────────────────────────────────────────────
|
||||
("ButtonInput", Swallowed),
|
||||
@@ -59,6 +59,14 @@ const VERDICTS: &[(&str, Verdict)] = &[
|
||||
"CursorOptions",
|
||||
Open("an OUTPUT (grab/visible), not a channel — the cover has nothing to take from it"),
|
||||
),
|
||||
(
|
||||
"RawWinitWindowEvent",
|
||||
Open(
|
||||
"read only to learn what the layout names each key (`bindings/layout.rs`), which acts \
|
||||
on nothing: a press under the cover still never reaches a binding or a box, and the \
|
||||
name it refreshes must be current when the cover lifts",
|
||||
),
|
||||
),
|
||||
(
|
||||
"CursorLeft",
|
||||
Open(
|
||||
@@ -76,6 +84,7 @@ const VERDICTS: &[(&str, Verdict)] = &[
|
||||
("CustomCursor", Plumbing),
|
||||
("CustomCursorImage", Plumbing),
|
||||
("InputPlugin", Plumbing),
|
||||
("WinitPlugin", Plumbing),
|
||||
("InputSystems", Plumbing),
|
||||
("Key", Plumbing),
|
||||
("KeyCode", Plumbing),
|
||||
@@ -174,11 +183,11 @@ fn every_input_channel_the_client_reads_has_a_verdict_under_the_cover() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Every input identifier `text` names: the leaf of any `bevy::input::…`/`bevy::window::…` path
|
||||
/// (grouped `use` braces expanded), plus the prelude names.
|
||||
/// Every input identifier `text` names: the leaf of any `bevy::input::…`/`bevy::window::…`/
|
||||
/// `bevy::winit::…` path (grouped `use` braces expanded), plus the prelude names.
|
||||
fn input_names(text: &str) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
for prefix in ["bevy::input::", "bevy::window::"] {
|
||||
for prefix in ["bevy::input::", "bevy::window::", "bevy::winit::"] {
|
||||
let mut rest = text;
|
||||
while let Some(at) = rest.find(prefix) {
|
||||
rest = &rest[at + prefix.len()..];
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
//! takes that named key's code, before Bevy's input collection, so every reader (the bindings,
|
||||
//! the text boxes, the glue screens, `ButtonInput<KeyCode>`) sees the layout's key. The Mac client
|
||||
//! reads its non-character keys off a fixed table on the physical keycode (`0x5bf320`), so macOS
|
||||
//! keeps them physical. Character keys stay physical, and so do the numpad keys, whose Num Lock
|
||||
//! navigation meanings are not remaps.
|
||||
//! keeps them physical. A letter, digit or punctuation key keeps its code too: 1.12 names it by
|
||||
//! the virtual key the layout makes it, a digit as itself (`0x42d81c`) and a letter or punctuation
|
||||
//! key through `MapVirtualKeyA` (`0x42da39`), which the game's key namer reads, not by a named
|
||||
//! key. So do the numpad keys, whose Num Lock navigation meanings are not remaps.
|
||||
|
||||
#[cfg(any(not(target_os = "macos"), test))]
|
||||
use bevy::input::keyboard::{Key, KeyCode};
|
||||
@@ -165,8 +167,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn characters_and_the_numpad_stay_physical() {
|
||||
// An azerty A: the key where a qwerty Q sits.
|
||||
fn characters_and_the_numpad_keep_their_codes() {
|
||||
// An azerty A, the key where a qwerty Q sits: named by its character, never moved.
|
||||
assert_eq!(
|
||||
layout_code(KeyCode::KeyQ, &Key::Character("a".into())),
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user