blob: 8a027529e42fca07ed124d14a840db9655a5212e [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.
//! This module contains an implementation of `ImeState`, the internal state object of `LegacyIme`.
use {
anyhow::{Context as _, Error},
fidl_fuchsia_input as input, fidl_fuchsia_ui_input as uii,
std::{
char,
collections::{HashMap, HashSet},
ops::Range,
},
};
use super::position;
use crate::{index_convert as idx, keyboard::events::KeyEvent, text_manager::TextManager};
/// The internal state of the IME, held within `LegacyIme` inside `Arc<Mutex<ImeState>>`, so it can
/// be accessed from multiple message handler async tasks. Methods that aren't message handlers
/// don't usually need to access `Arc<Mutex<LegacyIme>>` itself, and so can be put in here instead
/// of on `LegacyIme` itself.
pub struct ImeState {
pub text_state: uii::TextInputState,
/// A handle to call methods on the text field.
pub client: Box<dyn uii::InputMethodEditorClientProxyInterface>,
pub keyboard_type: uii::KeyboardType,
pub action: uii::InputMethodAction,
pub text_manager: TextManager,
/// Currently pressed keys.
pub keys_pressed: HashSet<input::Key>,
/// A number used to serve the TextField interface. It increments any time any
/// party makes a change to the state.
pub revision: u64,
/// A TextPoint is a u64 token that represents a character position in the new
/// TextField interface. Each token is a unique ID; this represents the ID we
/// will assign to the next TextPoint that is created. It increments every time
/// a TextPoint is created. This is never reset, even when TextPoints are
/// invalidated; TextPoints have globally unique IDs.
pub next_text_point_id: u64,
/// A TextPoint is a u64 token that represents a character position in the new
/// TextField interface. This maps TextPoint IDs to byte indexes inside
/// `text_state.text`. When a new revision is created, all preexisting TextPoints
/// are deleted, which means we clear this out.
pub text_points: HashMap<u64, usize>,
}
/// Looks up a TextPoint's byte index from a list of points. Usually this list will be
/// `ImeState.text_points`, but in the middle of a transaction, we clone it to a temporary list
/// so that we can mutate them without mutating the original list. That way, if the transaction
/// gets rejected, the original list is left intact.
impl ImeState {
/// Forwards a keyboard event to any listening clients without changing the actual state of the
/// IME at all.
pub(crate) fn forward_event(&mut self, key_event: KeyEvent) -> Result<(), Error> {
let keyboard_event: uii::KeyboardEvent =
key_event.try_into().context("error converting key event to keyboard event")?;
let state = idx::text_state_byte_to_codeunit(self.text_state.clone());
self.client
.did_update_state(&state, Some(&uii::InputEvent::Keyboard(keyboard_event)))
.with_context(|| {
format!(
"ImeState::forward_event: error sending state update to ImeClient: {:?}",
&keyboard_event
)
})?;
Ok(())
}
/// Any time the state is updated, this method is called, which allows ImeState to inform any
/// listening clients (either TextField or InputMethodEditorClientProxy) that state has updated.
/// If InputMethodEditorClient caused the update with SetState, set call_did_update_state so that
/// we don't send its own edit back to it. Otherwise, set to true.
pub fn increment_revision(&mut self, call_did_update_state: bool) {
self.revision += 1;
self.text_points = HashMap::new();
if call_did_update_state {
let state = idx::text_state_byte_to_codeunit(self.text_state.clone());
self.client.did_update_state(&state, None).unwrap_or_else(|e| {
tracing::warn!(
"ImeState::increment_revision: error sending state update to ImeClient: {:?}",
e
)
});
}
}
// gets start and len, and sets base/extent to start of string if don't exist
pub fn selection(&mut self) -> Range<usize> {
let s = &mut self.text_state.selection;
s.base = s.base.max(0).min(self.text_state.text.len() as i64);
s.extent = s.extent.max(0).min(self.text_state.text.len() as i64);
let start = s.base.min(s.extent) as usize;
let end = s.base.max(s.extent) as usize;
start..end
}
pub fn type_keycode(&mut self, code_point: u32) {
self.text_state.revision += 1;
let replacement = match char::from_u32(code_point) {
Some(v) => v.to_string(),
None => return,
};
let selection = self.selection();
self.text_state.text.replace_range(selection.clone(), &replacement);
self.text_state.selection.base = selection.start as i64 + replacement.len() as i64;
self.text_state.selection.extent = self.text_state.selection.base;
}
pub fn delete_backward(&mut self) {
self.text_state.revision += 1;
// set base and extent to 0 if either is -1, to ensure there is a selection/cursor
self.selection();
if self.text_state.selection.base == self.text_state.selection.extent {
// Select one grapheme or character to the left, so that it can be uniformly handled by
// the selection-deletion code below.
self.text_state.selection.base = position::adjacent_cursor_position(
&self.text_state.text,
self.text_state.selection.base as usize,
position::HorizontalMotion::GraphemeLeft(
position::GraphemeTraversal::CombiningCharacters,
),
) as i64;
}
self.delete_selection();
}
pub fn delete_forward(&mut self) {
self.text_state.revision += 1;
// Ensure valid selection/cursor.
self.selection();
if self.text_state.selection.base == self.text_state.selection.extent {
// Select one grapheme to the right so that it can be handled by the selection-deletion
// code below.
self.text_state.selection.extent = position::adjacent_cursor_position(
&self.text_state.text,
self.text_state.selection.base as usize,
position::HorizontalMotion::GraphemeRight,
) as i64;
}
self.delete_selection();
}
/// Deletes the selected text if the selection isn't empty.
/// Does not increment revision number. Should only be called from methods that do.
fn delete_selection(&mut self) {
// Delete the current selection.
let selection = self.selection();
if selection.start != selection.end {
self.text_state.text.replace_range(selection.clone(), "");
self.text_state.selection.extent = selection.start as i64;
self.text_state.selection.base = self.text_state.selection.extent;
}
}
pub fn cursor_horizontal_move(&mut self, modifiers: u32, go_right: bool) {
self.text_state.revision += 1;
let shift_pressed = modifiers & uii::MODIFIER_SHIFT != 0;
let ctrl_pressed = modifiers & uii::MODIFIER_CONTROL != 0;
let selection = self.selection();
let text_is_selected = selection.start != selection.end;
let mut new_position = self.text_state.selection.extent;
if !shift_pressed && text_is_selected {
// canceling selection, new position based on start/end of selection
if go_right {
new_position = selection.end as i64;
} else {
new_position = selection.start as i64;
}
if ctrl_pressed {
new_position = position::adjacent_cursor_position(
&self.text_state.text,
new_position as usize,
if go_right {
position::HorizontalMotion::WordRight
} else {
position::HorizontalMotion::WordLeft
},
) as i64;
}
} else {
// new position based previous value of extent
new_position = position::adjacent_cursor_position(
&self.text_state.text,
new_position as usize,
match (go_right, ctrl_pressed) {
(true, true) => position::HorizontalMotion::WordRight,
(false, true) => position::HorizontalMotion::WordLeft,
(true, false) => position::HorizontalMotion::GraphemeRight,
(false, false) => position::HorizontalMotion::GraphemeLeft(
position::GraphemeTraversal::WholeGrapheme,
),
},
) as i64;
}
self.text_state.selection.extent = new_position;
if !shift_pressed {
self.text_state.selection.base = new_position;
}
self.text_state.selection.affinity = uii::TextAffinity::Downstream;
}
}