blob: a9d37af2848c77c03c79357d48847686ab68b117 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{format_err, Context as _, Error};
use fidl_fuchsia_ui_input as uii;
use fidl_fuchsia_ui_input2 as ui_input;
use fidl_fuchsia_ui_text as txt;
use fuchsia_async as fasync;
use fuchsia_component::client::connect_to_service;
use fuchsia_component::server::ServiceFs;
use fuchsia_syslog::fx_log_err;
use futures::lock::Mutex;
use futures::prelude::*;
use serde_json::{self as json, Map, Value};
use std::collections::HashMap;
use std::collections::VecDeque;
use std::convert::TryInto;
use std::fs;
use std::sync::Arc;
use text::text_field_state::TextFieldStateLegacy;
mod keymap;
const ENABLE_TEXTFIELD: bool = false;
const LEGACY_LAYOUT_PATH: &'static str = "/pkg/data/us-legacy.json";
const MAX_QUEUED_INPUTS: usize = 100;
type DeadKeyMap = serde_json::map::Map<String, Value>;
// Keys specially handled by the IME.
// TODO(beckie): Move constants into common, centralized location?
const ENTER: u32 = 0x28;
const BACKSPACE: u32 = 0x2A;
const NUM_ENTER: u32 = 0x58;
struct DefaultHardwareImeState {
legacy_layout: Value,
current_field: Option<CurrentField>,
dead_key_state: Option<DeadKeyMap>,
unicode_input_mode: bool,
unicode_input_buffer: String,
input_queue: VecDeque<uii::KeyboardEvent>,
}
struct CurrentField {
proxy: txt::TextFieldLegacyProxy,
last_revision: u64,
last_selection: txt::Selection,
}
#[derive(Clone)]
struct DefaultHardwareIme(Arc<Mutex<DefaultHardwareImeState>>);
impl DefaultHardwareIme {
fn new() -> Result<DefaultHardwareIme, Error> {
let data = fs::read_to_string(LEGACY_LAYOUT_PATH)?;
let legacy_layout = json::from_str(&data)?;
let state = DefaultHardwareImeState {
legacy_layout,
current_field: None,
dead_key_state: None,
unicode_input_mode: false,
unicode_input_buffer: String::new(),
input_queue: VecDeque::new(),
};
Ok(DefaultHardwareIme(Arc::new(Mutex::new(state))))
}
fn on_focus(&self, text_field: txt::TextFieldLegacyProxy) {
let this = self.clone();
fasync::Task::spawn(async move {
let mut evt_stream = text_field.take_event_stream();
// wait for first onupdate to populate self.current_field
let res = evt_stream.next().await;
if let Some(Ok(txt::TextFieldLegacyEvent::OnUpdate { state })) = res {
let internal_state = match state.try_into() {
Ok(v) => v,
Err(e) => {
fx_log_err!("got invalid TextFieldStateLegacy: {}", e);
return;
}
};
this.0.lock().await.on_first_update(text_field, internal_state);
this.process_text_field_events(evt_stream).await.unwrap_or_else(|e| {
fx_log_err!("{}", e);
});
} else {
fx_log_err!("failed to get OnUpdate from newly focused TextField: {:?}", res);
}
})
.detach();
}
async fn process_text_field_events(
&self,
mut evt_stream: txt::TextFieldLegacyEventStream,
) -> Result<(), Error> {
while let Some(msg) = evt_stream.next().await {
match msg {
Ok(txt::TextFieldLegacyEvent::OnUpdate { state }) => {
let mut lock = self.0.lock().await;
lock.on_update(state.try_into()?);
lock.process_input_queue().await;
}
Err(e) => {
return Err(format_err!(
"error when receiving message from TextFieldLegacyEventStream: {}",
e
));
}
}
}
Ok(())
}
}
#[derive(Debug)]
enum OnInputError {
Retry,
Err(Error),
}
impl<E: Into<Error>> From<E> for OnInputError {
fn from(other: E) -> OnInputError {
OnInputError::Err(other.into())
}
}
impl DefaultHardwareImeState {
fn on_first_update(
&mut self,
text_field: txt::TextFieldLegacyProxy,
state: TextFieldStateLegacy,
) {
self.current_field = Some(CurrentField {
proxy: text_field,
last_revision: state.revision,
last_selection: state.selection,
});
}
fn on_update(&mut self, state: TextFieldStateLegacy) {
if let Some(s) = &mut self.current_field {
s.last_selection = state.selection;
s.last_revision = state.revision;
}
}
async fn process_input_queue(&mut self) {
while let Some(key) = self.input_queue.pop_front() {
match self.on_input_event(&key).await {
Ok(()) => {} // next
Err(OnInputError::Retry) => {
// put it back in queue and return
self.input_queue.push_front(key);
return;
}
Err(OnInputError::Err(e)) => {
fx_log_err!("{:?}", e);
}
}
}
}
/// Returns Err(OnInputError::Err) if something unrecoverably fails
/// Returns Err(OnInputError::Retry) if you should retry. In this case, it's important that we
/// *don't* mutate anything in self, so that retrying many times is idempotent.
/// Returns Ok(()) if edit was successfully committed.
async fn on_input_event<'a>(
&'a mut self,
event: &'a uii::KeyboardEvent,
) -> Result<(), OnInputError> {
// only process input events if there is an active text field
let field_state = match &mut self.current_field {
Some(v) => v,
None => return Ok(()),
};
if event.phase != uii::KeyboardEventPhase::Pressed
&& event.phase != uii::KeyboardEventPhase::Repeat
{
return Ok(());
}
// Handle unicode input mode keys
if self.unicode_input_mode {
match event.hid_usage {
BACKSPACE => {
// TODO: Set or reset composition highlight.
if let Some((index, _)) = self.unicode_input_buffer.char_indices().last() {
self.unicode_input_buffer.truncate(index);
}
return Ok(());
}
ENTER | NUM_ENTER => {
// Just support hex input for now.
// TODO: Expand to support character name input.
// TODO: Remove composition highlight.
let output = self
.unicode_input_buffer
.split(|c: char| c == '+' || c.is_whitespace())
.filter_map(parse_code_point)
.collect::<String>();
let mut range = clone_range(&field_state.last_selection.range);
field_state.proxy.begin_edit(field_state.last_revision)?;
field_state.proxy.replace(&mut range, &output)?;
convert_commit_result(field_state.proxy.commit_edit().await)?;
self.unicode_input_mode = false;
self.unicode_input_buffer = String::new();
return Ok(());
}
_ => {}
}
}
// Handle keys that do produce input (as determined by the layout).
match get_key_mapping(&self.legacy_layout, event) {
Err(e) => fx_log_err!("failed to find key mapping: {}", e),
Ok(Keymapping::Output(mut output)) => {
if let Some(dead_key) = &self.dead_key_state {
// TODO: Remove deadkey highlight.
if let Some(res) = dead_key[&output].as_str() {
output = res.to_string();
} else if let Some(res) = dead_key["\u{00A0}"].as_str() {
output = output + res;
} else if let Some(res) = dead_key["\u{0020}"].as_str() {
output = res.to_string() + &output;
}
}
if self.unicode_input_mode {
// TODO: Set or reset composition highlight.
self.unicode_input_buffer += &output;
} else {
let mut range = clone_range(&field_state.last_selection.range);
field_state.proxy.begin_edit(field_state.last_revision)?;
field_state.proxy.replace(&mut range, &output)?;
convert_commit_result(field_state.proxy.commit_edit().await)?;
}
self.dead_key_state = None;
}
Ok(Keymapping::Deadkey(deadkey)) => {
// TODO: Set or reset deadkey highlight.
self.dead_key_state = Some(deadkey);
}
Ok(Keymapping::UnicodeMode) => {
// TODO: Set or reset composition highlight.
self.unicode_input_mode = true;
self.unicode_input_buffer = String::new();
}
}
Ok(())
}
}
fn convert_commit_result(
fidl_result: Result<txt::ErrorLegacy, fidl::Error>,
) -> Result<(), OnInputError> {
match fidl_result {
Ok(e) => match e {
txt::ErrorLegacy::Ok => Ok(()),
txt::ErrorLegacy::BadRevision => Err(OnInputError::Retry),
e => Err(format_err!("DefaultHardwareIme received a Error: {:#?}", e).into()),
},
Err(e) => Err(format_err!("DefaultHardwareIme received a fidl::Error: {:#?}", e).into()),
}
}
fn get_key_mapping(legacy_layout: &Value, event: &uii::KeyboardEvent) -> Result<Keymapping, Error> {
let key = &event.hid_usage.to_string();
let mut current_modifiers = HashMap::new();
current_modifiers.insert("caps", (event.modifiers & uii::MODIFIER_CAPS_LOCK) != 0);
current_modifiers.insert("shift", (event.modifiers & uii::MODIFIER_SHIFT) != 0);
current_modifiers.insert("ctrl", (event.modifiers & uii::MODIFIER_CONTROL) != 0);
current_modifiers.insert("alt", (event.modifiers & uii::MODIFIER_ALT) != 0);
current_modifiers.insert("super", (event.modifiers & uii::MODIFIER_SUPER) != 0);
let tables = match legacy_layout["tables"].as_array() {
Some(v) => v,
None => return Err(format_err!("expected legacy_layout.tables to be a JSON array")),
};
for table in tables {
let modifiers = match table["modifiers"].as_object() {
Some(v) => v,
None => return Err(format_err!("expected table.modifiers to be a JSON object")),
};
let map = match table["map"].as_object() {
Some(v) => v,
None => return Err(format_err!("expected table.map to be a JSON object")),
};
if modifiers_match(&current_modifiers, modifiers) && map.contains_key(key) {
let key_obj = map[key].as_object();
if let Some(output) = key_obj.and_then(|m| m["output"].as_str()) {
return Ok(Keymapping::Output(output.to_string()));
} else if let Some(deadkey) = key_obj.and_then(|m| m["deadkey"].as_object()) {
return Ok(Keymapping::Deadkey(deadkey.clone()));
} else if let Some(_) = key_obj.and_then(|m| m["unicode"].as_str()) {
return Ok(Keymapping::UnicodeMode);
} else {
return Err(format_err!(
"expected object to either contain 'deadkey', 'output', or 'unicode' keys"
));
}
}
}
return Err(format_err!("couldn't find matching key in keymap"));
}
#[allow(dead_code)]
enum Keymapping {
/// This key combination indicates you should just output some text directly.
Output(String),
/// This key combination indicates you should use this dead key lookup table on the next typed
/// character.
Deadkey(DeadKeyMap),
/// This key combination indicates you should start arbitrary unicode insert mode
UnicodeMode,
}
#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
fuchsia_syslog::init_with_tags(&["default-hardware-ime"]).expect("syslog init should not fail");
serve_keymap(keymap::KeymapService::new()?).await.context("error serving keymap")?;
if ENABLE_TEXTFIELD {
let ime = DefaultHardwareIme::new()?;
fasync::Task::spawn(
serve_textfield(ime.clone())
.unwrap_or_else(|e: anyhow::Error| fx_log_err!("couldn't run: {:?}", e)),
)
.detach();
}
Ok(())
}
async fn serve_textfield(ime: DefaultHardwareIme) -> Result<(), Error> {
let text_service = connect_to_service::<txt::TextInputContextLegacyMarker>()?;
let mut evt_stream = text_service.take_event_stream();
while let Some(evt) = evt_stream.next().await {
match evt {
Ok(txt::TextInputContextLegacyEvent::OnFocus { text_field }) => {
ime.on_focus(text_field.into_proxy()?);
}
Ok(txt::TextInputContextLegacyEvent::OnInputEvent { event }) => match event {
uii::InputEvent::Keyboard(ke) => {
let mut lock = ime.0.lock().await;
// drop inputs if the queue is really long
if lock.input_queue.len() < MAX_QUEUED_INPUTS {
lock.input_queue.push_back(ke);
}
lock.process_input_queue().await;
}
_ => {
fx_log_err!("DefaultHardwareIme received a non-keyboard event");
}
},
Ok(txt::TextInputContextLegacyEvent::OnKey3Event { .. }) => {
unimplemented!();
}
Err(e) => {
fx_log_err!(
"DefaultHardwareIme received an error from a TextInputContext: {:#?}",
e
);
}
}
}
Ok(())
}
async fn serve_keymap(keymap_service: keymap::KeymapService) -> Result<(), Error> {
let mut fs = ServiceFs::new();
fs.dir("svc").add_fidl_service(|stream: ui_input::KeyboardLayoutStateRequestStream| {
let keymap_service = keymap_service.clone();
fuchsia_async::Task::spawn(
keymap::handle_watch_keymap(stream, keymap_service)
.unwrap_or_else(|e: anyhow::Error| fx_log_err!("couldn't run: {:?}", e)),
)
.detach();
});
fs.take_and_serve_directory_handle()?;
Ok(fs.collect().await)
}
fn clone_range(range: &txt::Range) -> txt::Range {
txt::Range {
start: txt::Position { id: range.start.id },
end: txt::Position { id: range.end.id },
}
}
fn parse_code_point(s: &str) -> Option<char> {
if s.is_empty() {
return None;
}
match u32::from_str_radix(s, 16) {
Err(_) => Some(std::char::REPLACEMENT_CHARACTER),
Ok(cc) => match std::char::from_u32(cc) {
None => Some(std::char::REPLACEMENT_CHARACTER),
Some(c) => Some(c),
},
}
}
fn modifiers_match(current: &HashMap<&'static str, bool>, to_match: &Map<String, Value>) -> bool {
for (key, value) in to_match {
match current.get(key.as_str()) {
Some(&other_value) => {
if value.as_bool() != Some(other_value) {
return false;
}
}
None => {
if value.as_bool() != None {
return false;
}
}
}
}
return true;
}
#[cfg(test)]
mod tests {
use super::*;
use futures::future::join;
fn default_state() -> TextFieldStateLegacy {
TextFieldStateLegacy {
document: txt::Range { start: txt::Position { id: 0 }, end: txt::Position { id: 0 } },
selection: txt::Selection {
range: txt::Range { start: txt::Position { id: 0 }, end: txt::Position { id: 0 } },
anchor: txt::SelectionAnchor::AnchoredAtStart,
affinity: txt::Affinity::Upstream,
},
revision: 0,
composition: None,
composition_highlight: None,
dead_key_highlight: None,
}
}
#[fasync::run_until_stalled(test)]
async fn state_sends_edits_on_input_and_retries() {
let ime = DefaultHardwareIme::new().unwrap();
let (proxy, server_end) =
fidl::endpoints::create_proxy::<txt::TextFieldLegacyMarker>().unwrap();
let mut request_stream = server_end.into_stream().unwrap();
let client = async move {
let mut lock = ime.0.lock().await;
// simulate onupdate
lock.on_first_update(proxy, default_state().into());
// dispatch input event, but drop input if we've already queued some wild number of
// events
lock.input_queue.push_back(uii::KeyboardEvent {
event_time: 0,
device_id: 0,
phase: uii::KeyboardEventPhase::Pressed,
hid_usage: 4,
code_point: 33,
modifiers: 0,
});
lock.process_input_queue().await;
// try again
lock.process_input_queue().await;
};
let server = async move {
// first set of edits, reply with BadRevision
let msg = request_stream.try_next().await.unwrap().unwrap();
match msg {
txt::TextFieldLegacyRequest::BeginEdit { .. } => {}
_ => panic!("expected first BeginEdit request"),
}
let msg = request_stream.try_next().await.unwrap().unwrap();
match msg {
txt::TextFieldLegacyRequest::Replace { new_text, .. } => {
assert_eq!("a", new_text);
}
_ => panic!("expected first Replace request"),
}
let msg = request_stream.try_next().await.unwrap().unwrap();
match msg {
txt::TextFieldLegacyRequest::CommitEdit { responder, .. } => {
responder.send(txt::ErrorLegacy::BadRevision).unwrap();
}
_ => panic!("expected first CommitEdit request"),
}
// second round of updates
let msg = request_stream.try_next().await.unwrap().unwrap();
match msg {
txt::TextFieldLegacyRequest::BeginEdit { .. } => {}
_ => panic!("expected second BeginEdit request"),
}
let msg = request_stream.try_next().await.unwrap().unwrap();
match msg {
txt::TextFieldLegacyRequest::Replace { new_text, .. } => {
assert_eq!("a", new_text);
}
_ => panic!("expected second Replace request"),
}
let msg = request_stream.try_next().await.unwrap().unwrap();
match msg {
txt::TextFieldLegacyRequest::CommitEdit { responder, .. } => {
responder.send(txt::ErrorLegacy::Ok).unwrap();
}
_ => panic!("expected second CommitEdit request"),
}
};
join(server, client).await;
}
}