| // 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(¤t_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; |
| } |
| } |