Android: Support unicode character mapping + dead keys

Up until now the Android backend has been directly mapping key codes
which essentially just represent the "physical" cap of the key (quoted
since this also related to virtual keyboards).

Since we didn't account for any meta keys either it meant the backend
only supported a 1:1 mapping from key codes, which only covers a tiny
subset of characters. For example you couldn't type a colon since
there's no keycode for that and we didn't try and map Shift+Semicolon
into a colon character.

This has been tricky to support because the `NativeActivity` class doesn't
have direct access to the Java `KeyEvent` object which exposes a more
convenient `getUnicodeChar` API.

It is now possible to query a `KeyCharcterMap` for the device associated
with a `KeyEvent` via the `AndroidApp::device_key_character_map` API
which provides a binding to the SDK `KeyCharacterMap` API in Java:

 https://developer.android.com/reference/android/view/KeyCharacterMap

This is effectively what `getUnicodeChar` is implemented based on and is
a bit more general purpose.

`KeyCharacterMap` lets us map a key_code + meta_state from a `KeyEvent`
into either a unicode character or dead key accent that can be combined
with the following key. This mapping is done based on the user's chosen
layout for the keyboard.

To enable support for key character maps the
`AndroidApp::input_events()` API was replaced by
`AndroidApp::input_events_iter()` which returns a (lending) iterator for
events. This was changed because the previous design made it difficult
to allow other AndroidApp APIs to be used while iterating events (mainly
because AndroidApp held a lock over the backend during iteration)
This commit is contained in:
Robert Bragg 2023-08-07 23:56:42 +01:00 committed by GitHub
parent e9ebf1e5f4
commit bd2f1e8312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 520 additions and 427 deletions

View file

@ -24,7 +24,6 @@ use crate::{
error,
event::{self, InnerSizeWriter, StartCause},
event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW},
keyboard::NativeKey,
platform::pump_events::PumpStatus,
window::{
self, CursorGrabMode, ImePurpose, ResizeDirection, Theme, WindowButtons, WindowLevel,
@ -151,6 +150,7 @@ pub struct EventLoop<T: 'static> {
control_flow: ControlFlow,
cause: StartCause,
ignore_volume_keys: bool,
combining_accent: Option<char>,
}
#[derive(Debug, Clone, PartialEq)]
@ -214,6 +214,7 @@ impl<T: 'static> EventLoop<T> {
control_flow: Default::default(),
cause: StartCause::Init,
ignore_volume_keys: attributes.ignore_volume_keys,
combining_accent: None,
}
}
@ -353,128 +354,25 @@ impl<T: 'static> EventLoop<T> {
trace!("No main event to handle");
}
// temporarily decouple `android_app` from `self` so we aren't holding
// a borrow of `self` while iterating
let android_app = self.android_app.clone();
// Process input events
self.android_app.input_events(|event| {
let mut input_status = InputStatus::Handled;
match event {
InputEvent::MotionEvent(motion_event) => {
let window_id = window::WindowId(WindowId);
let device_id = event::DeviceId(DeviceId);
match android_app.input_events_iter() {
Ok(mut input_iter) => loop {
let read_event = input_iter.next(|event| {
self.handle_input_event(&android_app, event, &mut control_flow, callback)
});
let phase = match motion_event.action() {
MotionAction::Down | MotionAction::PointerDown => {
Some(event::TouchPhase::Started)
}
MotionAction::Up | MotionAction::PointerUp => {
Some(event::TouchPhase::Ended)
}
MotionAction::Move => Some(event::TouchPhase::Moved),
MotionAction::Cancel => {
Some(event::TouchPhase::Cancelled)
}
_ => {
None // TODO mouse events
}
};
if let Some(phase) = phase {
let pointers: Box<
dyn Iterator<Item = android_activity::input::Pointer<'_>>,
> = match phase {
event::TouchPhase::Started
| event::TouchPhase::Ended => {
Box::new(
std::iter::once(motion_event.pointer_at_index(
motion_event.pointer_index(),
))
)
},
event::TouchPhase::Moved
| event::TouchPhase::Cancelled => {
Box::new(motion_event.pointers())
}
};
for pointer in pointers {
let location = PhysicalPosition {
x: pointer.x() as _,
y: pointer.y() as _,
};
trace!("Input event {device_id:?}, {phase:?}, loc={location:?}, pointer={pointer:?}");
let event = event::Event::WindowEvent {
window_id,
event: event::WindowEvent::Touch(
event::Touch {
device_id,
phase,
location,
id: pointer.pointer_id() as u64,
force: None,
},
),
};
sticky_exit_callback(
event,
self.window_target(),
&mut control_flow,
callback
);
}
}
}
InputEvent::KeyEvent(key) => {
match key.key_code() {
// Flag keys related to volume as unhandled. While winit does not have a way for applications
// to configure what keys to flag as handled, this appears to be a good default until winit
// can be configured.
Keycode::VolumeUp |
Keycode::VolumeDown |
Keycode::VolumeMute => {
if self.ignore_volume_keys {
input_status = InputStatus::Unhandled
}
},
keycode => {
let state = match key.action() {
KeyAction::Down => event::ElementState::Pressed,
KeyAction::Up => event::ElementState::Released,
_ => event::ElementState::Released,
};
let native = NativeKey::Android(keycode.into());
let logical_key = keycodes::to_logical(keycode, native);
// TODO: maybe use getUnicodeChar to get the logical key
let event = event::Event::WindowEvent {
window_id: window::WindowId(WindowId),
event: event::WindowEvent::KeyboardInput {
device_id: event::DeviceId(DeviceId),
event: event::KeyEvent {
state,
physical_key: keycodes::to_physical_keycode(keycode),
logical_key,
location: keycodes::to_location(keycode),
repeat: key.repeat_count() > 0,
text: None,
platform_specific: KeyEventExtra {},
},
is_synthetic: false,
},
};
sticky_exit_callback(
event,
self.window_target(),
&mut control_flow,
callback,
);
}
}
}
_ => {
warn!("Unknown android_activity input event {event:?}")
if !read_event {
break;
}
},
Err(err) => {
log::warn!("Failed to get input events iterator: {err:?}");
}
input_status
});
}
// Empty the user event buffer
{
@ -524,6 +422,117 @@ impl<T: 'static> EventLoop<T> {
self.pending_redraw = pending_redraw;
}
fn handle_input_event<F>(
&mut self,
android_app: &AndroidApp,
event: &InputEvent<'_>,
control_flow: &mut ControlFlow,
callback: &mut F,
) -> InputStatus
where
F: FnMut(event::Event<T>, &RootELW<T>, &mut ControlFlow),
{
let mut input_status = InputStatus::Handled;
match event {
InputEvent::MotionEvent(motion_event) => {
let window_id = window::WindowId(WindowId);
let device_id = event::DeviceId(DeviceId);
let phase = match motion_event.action() {
MotionAction::Down | MotionAction::PointerDown => {
Some(event::TouchPhase::Started)
}
MotionAction::Up | MotionAction::PointerUp => Some(event::TouchPhase::Ended),
MotionAction::Move => Some(event::TouchPhase::Moved),
MotionAction::Cancel => Some(event::TouchPhase::Cancelled),
_ => {
None // TODO mouse events
}
};
if let Some(phase) = phase {
let pointers: Box<dyn Iterator<Item = android_activity::input::Pointer<'_>>> =
match phase {
event::TouchPhase::Started | event::TouchPhase::Ended => {
Box::new(std::iter::once(
motion_event.pointer_at_index(motion_event.pointer_index()),
))
}
event::TouchPhase::Moved | event::TouchPhase::Cancelled => {
Box::new(motion_event.pointers())
}
};
for pointer in pointers {
let location = PhysicalPosition {
x: pointer.x() as _,
y: pointer.y() as _,
};
trace!("Input event {device_id:?}, {phase:?}, loc={location:?}, pointer={pointer:?}");
let event = event::Event::WindowEvent {
window_id,
event: event::WindowEvent::Touch(event::Touch {
device_id,
phase,
location,
id: pointer.pointer_id() as u64,
force: None,
}),
};
sticky_exit_callback(event, self.window_target(), control_flow, callback);
}
}
}
InputEvent::KeyEvent(key) => {
match key.key_code() {
// Flag keys related to volume as unhandled. While winit does not have a way for applications
// to configure what keys to flag as handled, this appears to be a good default until winit
// can be configured.
Keycode::VolumeUp | Keycode::VolumeDown | Keycode::VolumeMute => {
if self.ignore_volume_keys {
input_status = InputStatus::Unhandled
}
}
keycode => {
let state = match key.action() {
KeyAction::Down => event::ElementState::Pressed,
KeyAction::Up => event::ElementState::Released,
_ => event::ElementState::Released,
};
let key_char = keycodes::character_map_and_combine_key(
android_app,
key,
&mut self.combining_accent,
);
let event = event::Event::WindowEvent {
window_id: window::WindowId(WindowId),
event: event::WindowEvent::KeyboardInput {
device_id: event::DeviceId(DeviceId),
event: event::KeyEvent {
state,
physical_key: keycodes::to_physical_keycode(keycode),
logical_key: keycodes::to_logical(key_char, keycode),
location: keycodes::to_location(keycode),
repeat: key.repeat_count() > 0,
text: None,
platform_specific: KeyEventExtra {},
},
is_synthetic: false,
},
};
sticky_exit_callback(event, self.window_target(), control_flow, callback);
}
}
}
_ => {
warn!("Unknown android_activity input event {event:?}")
}
}
input_status
}
pub fn run<F>(mut self, event_handler: F) -> Result<(), RunLoopError>
where
F: FnMut(event::Event<T>, &event_loop::EventLoopWindowTarget<T>, &mut ControlFlow),