mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Type: Alt + numeric-keypad codes type their character on Windows; type.insert takes a code (#5108)
This commit is contained in:
@@ -51,6 +51,9 @@ impl eframe::App for App {
|
||||
// A pen contact with the barrel button held presses the secondary button (Windows Ink).
|
||||
#[cfg(windows)]
|
||||
windows_stylus::raw_input_hook(raw);
|
||||
// The characters Alt + keypad codes typed (#5108).
|
||||
#[cfg(windows)]
|
||||
super::windows_alt_codes::raw_input_hook(raw);
|
||||
file_access::unconfined(|| self.app.raw_input_hook(ctx, raw));
|
||||
// A mouse-up that never reached the window (#4326) is delivered once AppKit reports
|
||||
// that no mouse button is down.
|
||||
|
||||
@@ -56,6 +56,7 @@ mod system_fonts;
|
||||
mod tablet;
|
||||
mod window;
|
||||
mod window_frame;
|
||||
mod windows_alt_codes;
|
||||
#[cfg(windows)]
|
||||
mod windows_cores;
|
||||
mod windows_stylus;
|
||||
@@ -314,13 +315,18 @@ fn main() -> std::process::ExitCode {
|
||||
};
|
||||
// With Windows Ink, winit reports a pen as a touch and drops its barrel button and which end
|
||||
// is down; the message hook notes each pointer contact and whether it starts with the barrel
|
||||
// button held, and writes the pen's end into the pen feed (`windows_stylus`).
|
||||
// button held, and writes the pen's end into the pen feed (`windows_stylus`). It also notes the
|
||||
// characters Alt + keypad codes type (`windows_alt_codes`).
|
||||
#[cfg(windows)]
|
||||
let options = {
|
||||
use winit::platform::windows::EventLoopBuilderExtWindows as _;
|
||||
let mut options = options;
|
||||
options.event_loop_builder = Some(Box::new(|builder| {
|
||||
builder.with_msg_hook(windows_stylus::msg_hook);
|
||||
builder.with_msg_hook(|msg| {
|
||||
// Alt + keypad codes (#5108), which winit drops; neither hook consumes a message.
|
||||
windows_alt_codes::msg_hook(msg);
|
||||
windows_stylus::msg_hook(msg)
|
||||
});
|
||||
}));
|
||||
options
|
||||
};
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
//! Alt + numeric-keypad codes on Windows (#5108): holding Alt, typing a decimal code on the
|
||||
//! numeric keypad (NumLock on) and releasing Alt types that character (`Alt+0176` → `°`).
|
||||
//!
|
||||
//! Windows does the work: when Alt comes up after keypad digits, `TranslateMessage` posts a
|
||||
//! `WM_CHAR` holding the character the code stands for (in the system's ANSI code page for a code
|
||||
//! with a leading 0, else its OEM code page), the convention every Windows text field follows
|
||||
//! (<https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input>). winit
|
||||
//! 0.30 reads a `WM_CHAR` only as part of the key press before it, so it drops this one, which
|
||||
//! follows a key release, and egui never sees the text. winit's message hook ([`msg_hook`]) sees
|
||||
//! the message before winit handles it: [`AltCodes`] notes keypad digits typed with Alt held, takes
|
||||
//! the character that follows Alt's release, and [`raw_input_hook`] hands it to the frame as typed
|
||||
//! text, so the Type tool (or a panel's text field) types it like any other character, in the
|
||||
//! same undo step as the typing round it. The keypad digits themselves are taken out of the frame,
|
||||
//! so they don't run a shortcut bound to Alt and a digit.
|
||||
//!
|
||||
//! egui reports a keypad digit as the same key as the digit above the letters, so the other
|
||||
//! platforms, whose systems have no such convention, can't tell them apart either.
|
||||
|
||||
#![cfg_attr(not(windows), allow(dead_code))]
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use egui::{Event, Key};
|
||||
|
||||
const WM_KEYDOWN: u32 = 0x0100;
|
||||
const WM_KEYUP: u32 = 0x0101;
|
||||
const WM_CHAR: u32 = 0x0102;
|
||||
const WM_SYSKEYDOWN: u32 = 0x0104;
|
||||
const WM_SYSKEYUP: u32 = 0x0105;
|
||||
const WM_SYSCHAR: u32 = 0x0106;
|
||||
/// `VK_NUMPAD0` to `VK_NUMPAD9` (`winuser.h`): keypad digits with NumLock on. With it off the keys
|
||||
/// are Insert, End, … and type no code.
|
||||
const VK_NUMPAD0: usize = 0x60;
|
||||
const VK_NUMPAD9: usize = 0x69;
|
||||
/// `VK_MENU`, `VK_LMENU` and `VK_RMENU`: Alt.
|
||||
const ALT_KEYS: [usize; 3] = [0x12, 0xA4, 0xA5];
|
||||
|
||||
thread_local! {
|
||||
/// The UI thread's sequence: [`msg_hook`] and [`raw_input_hook`] both run on it.
|
||||
static CODES: RefCell<AltCodes> = RefCell::new(AltCodes::default());
|
||||
}
|
||||
|
||||
/// Where an Alt + keypad code stands, from the keyboard messages of the thread's queue.
|
||||
#[derive(Default)]
|
||||
struct AltCodes {
|
||||
/// Keypad digits typed while Alt has been held.
|
||||
digits: usize,
|
||||
/// Alt came up after keypad digits: the next character message is the code's character.
|
||||
armed: bool,
|
||||
/// A high surrogate waiting for its low half.
|
||||
high: Option<u16>,
|
||||
/// Characters the codes typed, for the next frame.
|
||||
typed: String,
|
||||
/// Keypad digit presses and releases with Alt held since the last frame, which egui reports
|
||||
/// as Alt and a digit: taken out of the frame.
|
||||
keypad_events: usize,
|
||||
}
|
||||
|
||||
impl AltCodes {
|
||||
/// Note a keyboard message `message` with `wparam` (the virtual-key code, or a UTF-16 unit).
|
||||
fn message(&mut self, message: u32, wparam: usize) {
|
||||
let keypad = (VK_NUMPAD0..=VK_NUMPAD9).contains(&wparam);
|
||||
match message {
|
||||
// Windows sends a key pressed with Alt held as a system key.
|
||||
WM_SYSKEYDOWN if keypad => {
|
||||
self.digits += 1;
|
||||
self.keypad_events += 1;
|
||||
self.armed = false;
|
||||
}
|
||||
WM_SYSKEYUP if keypad => self.keypad_events += 1,
|
||||
WM_KEYDOWN | WM_SYSKEYDOWN if ALT_KEYS.contains(&wparam) => {}
|
||||
// Any other key ends the sequence and keeps its own meaning.
|
||||
WM_KEYDOWN | WM_SYSKEYDOWN => self.reset(),
|
||||
WM_KEYUP | WM_SYSKEYUP if ALT_KEYS.contains(&wparam) => {
|
||||
self.armed = self.digits > 0;
|
||||
self.digits = 0;
|
||||
}
|
||||
WM_CHAR | WM_SYSCHAR if self.armed => {
|
||||
let unit = (wparam & 0xffff) as u16;
|
||||
if (0xd800..0xdc00).contains(&unit) {
|
||||
self.high = Some(unit);
|
||||
return;
|
||||
}
|
||||
let units: Vec<u16> = self.high.take().into_iter().chain([unit]).collect();
|
||||
self.typed.extend(char::decode_utf16(units).filter_map(Result::ok).filter(|c| !c.is_control()));
|
||||
self.armed = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.digits = 0;
|
||||
self.armed = false;
|
||||
self.high = None;
|
||||
}
|
||||
|
||||
/// Hand the frame `events` the characters typed since the last frame, as typed text, and take
|
||||
/// out the keypad digits typed with Alt held.
|
||||
fn drain(&mut self, events: &mut Vec<Event>) {
|
||||
let mut keypad = std::mem::take(&mut self.keypad_events);
|
||||
if keypad > 0 {
|
||||
events.retain(|e| {
|
||||
let digit = matches!(e, Event::Key { key: Key::Num0 | Key::Num1 | Key::Num2 | Key::Num3 | Key::Num4 | Key::Num5 | Key::Num6 | Key::Num7 | Key::Num8 | Key::Num9, modifiers, .. } if modifiers.alt);
|
||||
if digit && keypad > 0 {
|
||||
keypad -= 1;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
if !self.typed.is_empty() {
|
||||
events.push(Event::Text(std::mem::take(&mut self.typed)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// winit's message hook for the keyboard: notes Alt + keypad codes and the characters they type.
|
||||
/// It never consumes the message.
|
||||
#[cfg(windows)]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn msg_hook(msg: *const std::ffi::c_void) {
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
// SAFETY: winit passes the `MSG` that `PeekMessageW` has just filled, which lives for this
|
||||
// call; `as_ref` gives `None` for a null pointer.
|
||||
let Some(msg) = (unsafe { msg.cast::<MSG>().as_ref() }) else {
|
||||
return;
|
||||
};
|
||||
if (WM_KEYDOWN..=WM_SYSCHAR).contains(&msg.message) {
|
||||
CODES.with(|c| {
|
||||
if let Ok(mut c) = c.try_borrow_mut() {
|
||||
c.message(msg.message, msg.wParam.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Before egui reads the main window's frame: the characters Alt + keypad codes typed, as text.
|
||||
pub fn raw_input_hook(raw: &mut egui::RawInput) {
|
||||
CODES.with(|c| {
|
||||
if let Ok(mut c) = c.try_borrow_mut() {
|
||||
c.drain(&mut raw.events);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ALT: usize = 0x12;
|
||||
|
||||
/// Alt held, keypad digits `code`, Alt released, and the character Windows then posts.
|
||||
fn alt_code(c: &mut AltCodes, code: &str, posted: u16) {
|
||||
c.message(WM_SYSKEYDOWN, ALT);
|
||||
for d in code.bytes() {
|
||||
let vk = VK_NUMPAD0 + usize::from(d - b'0');
|
||||
c.message(WM_SYSKEYDOWN, vk);
|
||||
c.message(WM_SYSKEYUP, vk);
|
||||
}
|
||||
c.message(WM_KEYUP, ALT);
|
||||
c.message(WM_CHAR, usize::from(posted));
|
||||
}
|
||||
|
||||
fn key(key: Key, alt: bool, pressed: bool) -> Event {
|
||||
let modifiers = egui::Modifiers { alt, ..Default::default() };
|
||||
Event::Key { key, physical_key: Some(key), pressed, repeat: false, modifiers }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_alt_code_types_its_character_when_alt_comes_up() {
|
||||
let mut c = AltCodes::default();
|
||||
alt_code(&mut c, "0176", 0xB0);
|
||||
alt_code(&mut c, "0151", 0x2014);
|
||||
let mut events = vec![key(Key::Num0, true, true), key(Key::Num0, true, false), key(Key::Num1, false, true)];
|
||||
c.drain(&mut events);
|
||||
// The keypad digits typed with Alt are gone (eight presses and eight releases, two here);
|
||||
// the digit typed without Alt stays, and the characters follow.
|
||||
assert_eq!(events, vec![key(Key::Num1, false, true), Event::Text("°—".into())]);
|
||||
let mut events = vec![key(Key::Num0, true, true)];
|
||||
c.drain(&mut events);
|
||||
assert_eq!(events.len(), 1, "counts don't carry over to a later frame");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_character_without_a_code_before_it_is_left_to_winit() {
|
||||
let mut c = AltCodes::default();
|
||||
// A plain key press's character (winit types it), and Alt pressed and released alone.
|
||||
c.message(WM_KEYDOWN, 0x41);
|
||||
c.message(WM_CHAR, usize::from(b'a'));
|
||||
c.message(WM_SYSKEYDOWN, ALT);
|
||||
c.message(WM_KEYUP, ALT);
|
||||
c.message(WM_CHAR, usize::from(b'b'));
|
||||
let mut events = vec![];
|
||||
c.drain(&mut events);
|
||||
assert!(events.is_empty(), "{events:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_key_while_alt_is_held_cancels_the_code() {
|
||||
let mut c = AltCodes::default();
|
||||
c.message(WM_SYSKEYDOWN, ALT);
|
||||
c.message(WM_SYSKEYDOWN, VK_NUMPAD0 + 1);
|
||||
// Alt+F: a shortcut, not a code.
|
||||
c.message(WM_SYSKEYDOWN, 0x46);
|
||||
c.message(WM_KEYUP, ALT);
|
||||
c.message(WM_CHAR, 0x41);
|
||||
let mut events = vec![];
|
||||
c.drain(&mut events);
|
||||
assert!(!events.iter().any(|e| matches!(e, Event::Text(_))), "{events:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_code_above_the_basic_plane_arrives_as_two_halves() {
|
||||
let mut c = AltCodes::default();
|
||||
c.message(WM_SYSKEYDOWN, ALT);
|
||||
c.message(WM_SYSKEYDOWN, VK_NUMPAD0 + 1);
|
||||
c.message(WM_KEYUP, ALT);
|
||||
c.message(WM_CHAR, 0xD83D);
|
||||
c.message(WM_CHAR, 0xDE00);
|
||||
let mut events = vec![];
|
||||
c.drain(&mut events);
|
||||
assert_eq!(events, vec![Event::Text("😀".into())]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user