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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions crates/warpui/src/windowing/winit/event_loop/key_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,22 @@ pub fn convert_keyboard_input_event(
.modifiers
.contains(ModifiersState::CONTROL | ModifiersState::ALT) =>
{
input.key_without_modifiers()
let kwm = input.key_without_modifiers();
if !matches!(kwm, Key::Unidentified(_)) {
kwm
} else if let Some(text) = input.text_with_all_modifiers() {
Key::Character(text.to_lowercase().into())
} else {
kwm
}
}
// RDP Unicode input mode (Windows App for iPad): both physical_key and logical_key
// are Unidentified because the RDP client sends Unicode events without scancodes.
// The only reliable source of the typed character is input.text. We synthesize a
// Key::Character from it so the rest of the pipeline can handle it normally.
#[cfg(windows)]
Key::Unidentified(NativeKey::Windows(_)) if input.text.is_some() => {
rdp_key_from_text(input.text.as_deref().unwrap())
}
// On Windows, non-Latin keyboard layouts (Cyrillic, Greek, Arabic, etc.) translate
// the physical key to a non-ASCII character even when Ctrl/Cmd is held. That makes
Expand Down Expand Up @@ -153,12 +168,33 @@ pub fn convert_keyboard_input_event(
})
}

/// Synthesizes a `Key::Character` from raw text in RDP Unicode input mode.
///
/// In RDP Unicode mode (Windows App for iPad) both `physical_key` and `logical_key`
/// are `Unidentified`; `input.text` is the only source of the typed character.
/// We lowercase the text so that `get_input_key` can re-apply case based on the
/// current `shift` state, matching the behaviour of normal Scancode-mode input.
#[cfg(windows)]
pub(super) fn rdp_key_from_text(text: &str) -> Key {
Key::Character(text.to_lowercase().into())
}

#[cfg(not(target_family = "wasm"))]
/// Returns the base key without any modifiers applied, or `None` if it cannot be determined.
fn get_key_without_modifiers(input: &winit::event::KeyEvent) -> Option<String> {
let unmodified = input.key_without_modifiers();
let unmodified_input = get_input_key(&unmodified, false);
convert_key(unmodified_input).map(|k| k.to_string())
if let Some(key) = convert_key(unmodified_input).map(|k| k.to_string()) {
return Some(key);
}
// Fallback for RDP Unicode mode: both physical_key and logical_key are Unidentified,
// so key_without_modifiers() also returns Unidentified. Use input.text lowercased
// as a best-effort base key (e.g. Shift+A sends text="A" → base "a").
#[cfg(windows)]
if let Some(text) = &input.text {
return convert_key(rdp_key_from_text(text)).map(|k| k.to_string());
}
None
}

#[cfg(target_family = "wasm")]
Expand Down Expand Up @@ -211,7 +247,7 @@ fn get_input_key(logical_key: &Key, is_shift: bool) -> Key {

/// Converts a winit [`winit::keyboard::Key`] to the corresponding string version
/// expected by the UI framework.
fn convert_key(key: Key) -> Option<Cow<'static, str>> {
pub(super) fn convert_key(key: Key) -> Option<Cow<'static, str>> {
use winit::keyboard::Key::*;

let value = match key {
Expand Down
76 changes: 74 additions & 2 deletions crates/warpui/src/windowing/winit/event_loop/key_events_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::{get_input_key, us_qwerty_fallback_for_chord};
use winit::keyboard::{Key::Character, KeyCode, NativeKeyCode, PhysicalKey, SmolStr};
use super::{convert_key, get_input_key, us_qwerty_fallback_for_chord};
#[cfg(windows)]
use super::rdp_key_from_text;
use winit::keyboard::{Key, Key::Character, KeyCode, NativeKeyCode, PhysicalKey, SmolStr};

#[test]
fn test_get_input_key() {
Expand Down Expand Up @@ -157,3 +159,73 @@ fn us_qwerty_fallback_returns_none_for_unidentified_physical_key() {
);
}
}

// Tests for RDP Unicode mode fallback: exercises the Unidentified+text path
// via rdp_key_from_text, which is the exact function called when both
// physical_key and logical_key are Unidentified(Windows(_)) and text is Some.
#[cfg(windows)]
#[test]
fn rdp_key_from_text_produces_lowercase_character() {
// Verify that rdp_key_from_text lowercases the input so that get_input_key
// can re-apply case from the shift modifier state downstream.
let cases = [
("h", "h"),
("H", "h"), // Shift held — text arrives as uppercase, must be lowercased
("k", "k"),
("1", "1"),
("!", "!"), // punctuation — no case change
];
for (text, expected) in cases {
let key = rdp_key_from_text(text);
let result = convert_key(key);
assert_eq!(
result.as_deref(),
Some(expected),
"rdp_key_from_text({text:?}) should produce key '{expected}'",
);
}
}

#[cfg(windows)]
#[test]
fn rdp_key_from_text_roundtrips_through_get_input_key_without_shift() {
// Without shift, get_input_key should lowercase — same as rdp_key_from_text output.
let key = rdp_key_from_text("h");
let input_key = get_input_key(&key, false);
let result = convert_key(input_key);
assert_eq!(result.as_deref(), Some("h"));
}

#[cfg(windows)]
#[test]
fn rdp_key_from_text_roundtrips_through_get_input_key_with_shift() {
// With shift held, get_input_key should uppercase the lowercased character,
// producing the correct final key for shift+letter chords.
let key = rdp_key_from_text("H"); // Shift+H arrives as "H" in text
let input_key = get_input_key(&key, true); // shift=true
let result = convert_key(input_key);
assert_eq!(result.as_deref(), Some("H"));
}

#[test]
fn rdp_unicode_mode_named_keys_still_convert() {
// Named keys (Enter, Escape, arrows) go through convert_key via the Named
// variant and must work regardless of physical_key being Unidentified.
use winit::keyboard::NamedKey;
let cases = [
(Key::Named(NamedKey::Enter), "enter"),
(Key::Named(NamedKey::Escape), "escape"),
(Key::Named(NamedKey::ArrowUp), "up"),
(Key::Named(NamedKey::ArrowDown), "down"),
(Key::Named(NamedKey::Tab), "tab"),
(Key::Named(NamedKey::Backspace), "backspace"),
];
for (key, expected) in cases {
let result = convert_key(key.clone());
assert_eq!(
result.as_deref(),
Some(expected),
"convert_key({key:?}) should return '{expected}'",
);
}
}
84 changes: 84 additions & 0 deletions crates/warpui/src/windowing/winit/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ fn try_from_winit_keycode(keycode: &KeyCode) -> Result<crate::platform::keyboard
}
}

/// Maps a Windows Virtual Key code to a modifier KeyCode. Used as a fallback when
/// `physical_key` is `Unidentified` (e.g. RDP Unicode input mode where the Windows App
/// client on iPad sends Unicode events without scancodes).
#[cfg(windows)]
fn try_from_windows_vk(vk: u16) -> Result<crate::platform::keyboard::KeyCode, ()> {
use crate::platform::keyboard::KeyCode;
match vk as u32 {
0xA0 => Ok(KeyCode::ShiftLeft),
0xA1 => Ok(KeyCode::ShiftRight),
0xA2 => Ok(KeyCode::ControlLeft),
0xA3 => Ok(KeyCode::ControlRight),
0xA4 => Ok(KeyCode::AltLeft),
0xA5 => Ok(KeyCode::AltRight),
0x5B => Ok(KeyCode::SuperLeft),
0x5C => Ok(KeyCode::SuperRight),
// Generic (non-sided) VK codes — map to left variant as safe fallback
0x10 => Ok(KeyCode::ShiftLeft),
0x11 => Ok(KeyCode::ControlLeft),
0x12 => Ok(KeyCode::AltLeft),
_ => Err(()),
}
}

/// Data needed to detect double/triple-click.
struct MouseButtonPressState {
pressed_at: Instant,
Expand Down Expand Up @@ -1283,6 +1306,19 @@ impl EventLoop {
_ => {}
}
}
// Fallback for RDP Unicode mode: physical_key is Unidentified, so use
// the Windows VK code directly to track left/right Alt state.
#[cfg(windows)]
if let keyboard::PhysicalKey::Unidentified(keyboard::NativeKeyCode::Windows(vk)) =
&event.physical_key
{
let is_pressed = event.state == ElementState::Pressed;
match *vk as u32 {
0xA4 => window_state.left_alt_pressed = is_pressed,
0xA5 => window_state.right_alt_pressed = is_pressed,
_ => {}
}
}

// If the event is a modifier key, just by itself, we handle it specially, issuing
// the appropriate Warp-side event (ModifierKeyChanged).
Expand All @@ -1296,6 +1332,19 @@ impl EventLoop {
});
}
}
// Fallback for RDP Unicode mode: emit ModifierKeyChanged using Windows VK
// codes when physical_key is Unidentified and text is None (pure modifier key).
#[cfg(windows)]
if let (None, keyboard::PhysicalKey::Unidentified(keyboard::NativeKeyCode::Windows(vk))) =
(&event.text, &event.physical_key)
{
if let Ok(mapped_keycode) = try_from_windows_vk(*vk) {
return Some(ConvertedEvent::ModifierKeyChanged {
key_code: mapped_keycode,
state: event.state,
});
}
}

let event_text = event.text.as_ref().map(|text| text.to_string());
let warp_ui_event =
Expand Down Expand Up @@ -2043,6 +2092,41 @@ enum ConvertedEvent {
},
}

#[cfg(test)]
#[cfg(windows)]
mod rdp_vk_tests {
use super::try_from_windows_vk;
use crate::platform::keyboard::KeyCode;

#[test]
fn sided_modifier_vk_codes() {
assert_eq!(try_from_windows_vk(0xA0), Ok(KeyCode::ShiftLeft));
assert_eq!(try_from_windows_vk(0xA1), Ok(KeyCode::ShiftRight));
assert_eq!(try_from_windows_vk(0xA2), Ok(KeyCode::ControlLeft));
assert_eq!(try_from_windows_vk(0xA3), Ok(KeyCode::ControlRight));
assert_eq!(try_from_windows_vk(0xA4), Ok(KeyCode::AltLeft));
assert_eq!(try_from_windows_vk(0xA5), Ok(KeyCode::AltRight));
assert_eq!(try_from_windows_vk(0x5B), Ok(KeyCode::SuperLeft));
assert_eq!(try_from_windows_vk(0x5C), Ok(KeyCode::SuperRight));
}

#[test]
fn generic_modifier_vk_codes_map_to_left_variant() {
assert_eq!(try_from_windows_vk(0x10), Ok(KeyCode::ShiftLeft));
assert_eq!(try_from_windows_vk(0x11), Ok(KeyCode::ControlLeft));
assert_eq!(try_from_windows_vk(0x12), Ok(KeyCode::AltLeft));
}

#[test]
fn non_modifier_vk_codes_return_err() {
// VK_C (character key), VK_RETURN, VK_ESCAPE
assert_eq!(try_from_windows_vk(0x43), Err(()));
assert_eq!(try_from_windows_vk(0x0D), Err(()));
assert_eq!(try_from_windows_vk(0x1B), Err(()));
assert_eq!(try_from_windows_vk(0x00), Err(()));
}
}

/// Convert the platform-independent trait object Window into a concrete, platform-specific Window.
fn downcast_window(window: &dyn platform::Window) -> &super::Window {
window
Expand Down