blob: bee860e1be33ccc55f4e41333cef69a1c36799d5 [file] [log] [blame]
// Copyright 2018 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 crate::ime_service::ImeService;
use failure::ResultExt;
use fidl::encoding::OutOfLine;
use fidl::endpoints::RequestStream;
use fidl_fuchsia_ui_input as uii;
use fidl_fuchsia_ui_input::InputMethodEditorRequest as ImeReq;
use fuchsia_syslog::{fx_log_err, fx_log_warn};
use futures::prelude::*;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use regex::Regex;
use std::char;
use std::ops::Range;
use std::sync::{Arc, Weak};
use unicode_segmentation::GraphemeCursor;
// TODO(lard): move constants into common, centralized location?
pub const HID_USAGE_KEY_BACKSPACE: u32 = 0x2a;
pub const HID_USAGE_KEY_RIGHT: u32 = 0x4f;
pub const HID_USAGE_KEY_LEFT: u32 = 0x50;
pub const HID_USAGE_KEY_ENTER: u32 = 0x28;
pub const HID_USAGE_KEY_DELETE: u32 = 0x2e;
/// The internal state of the IME, usually held within the IME behind an Arc<Mutex>
/// so it can be accessed from multiple places.
pub struct ImeState {
text_state: uii::TextInputState,
client: Box<uii::InputMethodEditorClientProxyInterface>,
keyboard_type: uii::KeyboardType,
action: uii::InputMethodAction,
ime_service: ImeService,
}
/// A service that talks to a text field, providing it edits and cursor state updates
/// in response to user input.
#[derive(Clone)]
pub struct Ime(Arc<Mutex<ImeState>>);
impl Ime {
pub fn new<I: 'static + uii::InputMethodEditorClientProxyInterface>(
keyboard_type: uii::KeyboardType, action: uii::InputMethodAction,
initial_state: uii::TextInputState, client: I, ime_service: ImeService,
) -> Ime {
let state = ImeState {
text_state: initial_state,
client: Box::new(client),
keyboard_type,
action,
ime_service,
};
Ime(Arc::new(Mutex::new(state)))
}
pub fn downgrade(&self) -> Weak<Mutex<ImeState>> {
Arc::downgrade(&self.0)
}
pub fn upgrade(weak: &Weak<Mutex<ImeState>>) -> Option<Ime> {
weak.upgrade().map(|arc| Ime(arc))
}
pub fn bind_ime(&self, chan: fuchsia_async::Channel) {
let self_clone = self.clone();
let self_clone_2 = self.clone();
fuchsia_async::spawn(
async move {
let mut stream = uii::InputMethodEditorRequestStream::from_channel(chan);
while let Some(msg) = await!(stream.try_next())
.context("error reading value from IME request stream")?
{
match msg {
ImeReq::SetKeyboardType { keyboard_type, .. } => {
let mut state = self_clone.0.lock();
state.keyboard_type = keyboard_type;
}
ImeReq::SetState { state, .. } => {
self_clone.set_state(state);
}
ImeReq::InjectInput { event, .. } => {
self_clone.inject_input(event);
}
ImeReq::Show { .. } => {
// clone to ensure we only hold one lock at a time
let ime_service = self_clone.0.lock().ime_service.clone();
ime_service.show_keyboard();
}
ImeReq::Hide { .. } => {
// clone to ensure we only hold one lock at a time
let ime_service = self_clone.0.lock().ime_service.clone();
ime_service.hide_keyboard();
}
}
}
Ok(())
}
.unwrap_or_else(|e: failure::Error| fx_log_err!("{:?}", e))
.then(async move |()| {
// this runs when IME stream closes
// clone to ensure we only hold one lock at a time
let ime_service = self_clone_2.0.lock().ime_service.clone();
ime_service.update_keyboard_visibility_from_ime(&self_clone_2.0, false);
}),
);
}
fn set_state(&self, input_state: uii::TextInputState) {
self.0.lock().text_state = input_state;
// the old C++ IME implementation didn't call did_update_state here, so we won't either.
}
pub fn inject_input(&self, event: uii::InputEvent) {
let mut state = self.0.lock();
let keyboard_event = match event {
uii::InputEvent::Keyboard(e) => e,
_ => return,
};
if keyboard_event.phase == uii::KeyboardEventPhase::Pressed
|| keyboard_event.phase == uii::KeyboardEventPhase::Repeat
{
if keyboard_event.code_point != 0 {
state.type_keycode(keyboard_event.code_point);
state.did_update_state(keyboard_event)
} else {
match keyboard_event.hid_usage {
HID_USAGE_KEY_BACKSPACE => {
state.delete_backward();
state.did_update_state(keyboard_event);
}
HID_USAGE_KEY_DELETE => {
state.delete_forward();
state.did_update_state(keyboard_event);
}
HID_USAGE_KEY_LEFT => {
state.cursor_horizontal_move(keyboard_event.modifiers, false);
state.did_update_state(keyboard_event);
}
HID_USAGE_KEY_RIGHT => {
state.cursor_horizontal_move(keyboard_event.modifiers, true);
state.did_update_state(keyboard_event);
}
HID_USAGE_KEY_ENTER => {
state.client.on_action(state.action).unwrap_or_else(|e| {
fx_log_warn!("error sending action to ImeClient: {:?}", e)
});
}
_ => {
// Not an editing key, forward the event to clients.
state.did_update_state(keyboard_event);
}
}
}
}
}
}
/// Horizontal motion type for the cursor.
enum HorizontalMotion {
GraphemeLeft(GraphemeTraversal),
GraphemeRight,
}
/// How the cursor should traverse grapheme clusters.
enum GraphemeTraversal {
/// Move by whole grapheme clusters at a time.
///
/// This traversal mode should be used when using arrow keys, or when deleting forward (with the
/// <kbd>Delete</kbd> key).
WholeGrapheme,
/// Generally move by whole grapheme clusters, but allow moving through individual combining
/// characters, if present at the end of the grapheme cluster.
///
/// This traversal mode should be used when deleting backward (<kbd>Backspace</kbd>), but not
/// when deleting forward or using arrow keys.
///
/// This ensures that when a user is typing text and composes a character out of individual
/// combining diacritics, it should be possible to correct a mistake by pressing
/// <kbd>Backspace</kbd>. If we were to allow _moving the cursor_ left and right through
/// diacritics, that would only cause user confusion, as the blinking caret would not move
/// visibly while within a single grapheme cluster.
CombiningCharacters,
}
impl ImeState {
pub fn did_update_state(&mut self, e: uii::KeyboardEvent) {
self.client
.did_update_state(
&mut self.text_state,
Some(OutOfLine(&mut uii::InputEvent::Keyboard(e))),
)
.unwrap_or_else(|e| fx_log_warn!("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;
}
/// Calculates an adjacent cursor position to left or right of the current position.
///
/// * `start`: Starting position in the string, as a byte offset.
/// * `motion`: Whether to go right or left, and whether to allow entering grapheme clusters.
fn adjacent_cursor_position(&self, start: usize, motion: HorizontalMotion) -> usize {
let text_length = self.text_state.text.len();
let mut cursor = GraphemeCursor::new(start, text_length, true);
if let HorizontalMotion::GraphemeRight = motion {
let next_boundary = cursor.next_boundary(&self.text_state.text, 0);
match next_boundary {
Ok(Some(offset)) => return offset,
// Can't go right from the end of the string.
_ => return text_length,
}
}
let prev_boundary = cursor.prev_boundary(&self.text_state.text, 0);
if let Ok(Some(offset)) = prev_boundary {
if let HorizontalMotion::GraphemeLeft(GraphemeTraversal::CombiningCharacters) = motion {
let grapheme_str = &self.text_state.text[offset..start];
let last_char_str = match grapheme_str.char_indices().last() {
Some((last_char_offset, _c)) => Some(&grapheme_str[last_char_offset..]),
None => None,
};
if let Some(last_char_str) = last_char_str {
lazy_static! {
/// A regex that matches combining characters, e.g. accents and other
/// diacritics. Rust does not provide a way to check the Unicode categories
/// of `char`s directly, so this is the simplest workaround for now.
static ref COMBINING_REGEX: Regex = Regex::new(r"\p{M}$").unwrap();
}
if COMBINING_REGEX.is_match(last_char_str) {
return start - last_char_str.len();
}
}
}
return offset;
}
// Can't go left from the beginning of the string.
return 0;
}
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 = self.adjacent_cursor_position(
self.text_state.selection.base as usize,
HorizontalMotion::GraphemeLeft(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 = self.adjacent_cursor_position(
self.text_state.selection.base as usize,
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 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;
}
} else {
// new position based previous value of extent
new_position = self.adjacent_cursor_position(
new_position as usize,
if go_right {
HorizontalMotion::GraphemeRight
} else {
HorizontalMotion::GraphemeLeft(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;
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_helpers::{clone_state, default_state};
use fidl;
use fuchsia_zircon as zx;
use std::sync::mpsc::{channel, Receiver, Sender};
fn set_up(
text: &str, base: i64, extent: i64,
) -> (
Ime,
Receiver<uii::TextInputState>,
Receiver<uii::InputMethodAction>,
) {
let (client, statechan, actionchan) = MockImeClient::new();
let mut state = default_state();
state.text = text.to_string();
state.selection.base = base;
state.selection.extent = extent;
let ime = Ime::new(
uii::KeyboardType::Text,
uii::InputMethodAction::Search,
state,
client,
ImeService::new(),
);
(ime, statechan, actionchan)
}
fn simulate_keypress<K: Into<u32> + Copy>(
ime: &mut Ime, key: K, hid_key: bool, shift_pressed: bool,
) {
let hid_usage = if hid_key { key.into() } else { 0 };
let code_point = if hid_key { 0 } else { key.into() };
ime.inject_input(uii::InputEvent::Keyboard(uii::KeyboardEvent {
event_time: 0,
device_id: 0,
phase: uii::KeyboardEventPhase::Pressed,
hid_usage: hid_usage,
code_point: code_point,
modifiers: if shift_pressed {
uii::MODIFIER_SHIFT
} else {
0
},
}));
ime.inject_input(uii::InputEvent::Keyboard(uii::KeyboardEvent {
event_time: 0,
device_id: 0,
phase: uii::KeyboardEventPhase::Released,
hid_usage: hid_usage,
code_point: code_point,
modifiers: if shift_pressed {
uii::MODIFIER_SHIFT
} else {
0
},
}));
}
struct MockImeClient {
pub state: Mutex<Sender<uii::TextInputState>>,
pub action: Mutex<Sender<uii::InputMethodAction>>,
}
impl MockImeClient {
fn new() -> (
MockImeClient,
Receiver<uii::TextInputState>,
Receiver<uii::InputMethodAction>,
) {
let (s_send, s_rec) = channel();
let (a_send, a_rec) = channel();
let client = MockImeClient {
state: Mutex::new(s_send),
action: Mutex::new(a_send),
};
(client, s_rec, a_rec)
}
}
impl uii::InputMethodEditorClientProxyInterface for MockImeClient {
fn did_update_state(
&self, state: &mut uii::TextInputState,
mut _event: Option<fidl::encoding::OutOfLine<uii::InputEvent>>,
) -> Result<(), fidl::Error> {
let state2 = clone_state(state);
self.state
.lock()
.send(state2)
.map_err(|_| fidl::Error::ClientWrite(zx::Status::PEER_CLOSED))
}
fn on_action(&self, action: uii::InputMethodAction) -> Result<(), fidl::Error> {
self.action
.lock()
.send(action)
.map_err(|_| fidl::Error::ClientWrite(zx::Status::PEER_CLOSED))
}
}
#[test]
fn test_mock_ime_channels() {
let (client, statechan, actionchan) = MockImeClient::new();
let mut ime = Ime::new(
uii::KeyboardType::Text,
uii::InputMethodAction::Search,
default_state(),
client,
ImeService::new(),
);
assert_eq!(true, statechan.try_recv().is_err());
assert_eq!(true, actionchan.try_recv().is_err());
simulate_keypress(&mut ime, 'a', false, false);
assert_eq!(false, statechan.try_recv().is_err());
assert_eq!(true, actionchan.try_recv().is_err());
}
#[test]
fn test_delete_backward_empty_string() {
let (mut ime, statechan, _actionchan) = set_up("", -1, -1);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
// a second delete still does nothing, but increments revision
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_delete_forward_empty_string() {
let (mut ime, statechan, _actionchan) = set_up("", -1, -1);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
// a second delete still does nothing, but increments revision
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_delete_backward_beginning_string() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 0, 0);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_delete_forward_beginning_string() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 0, 0);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("bcdefghi", state.text);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_delete_backward_first_char_selected() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 0, 1);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("bcdefghi", state.text);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_delete_forward_last_char_selected() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 8, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefgh", state.text);
assert_eq!(8, state.selection.base);
assert_eq!(8, state.selection.extent);
}
#[test]
fn test_delete_backward_end_string() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 9, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefgh", state.text);
assert_eq!(8, state.selection.base);
assert_eq!(8, state.selection.extent);
}
#[test]
fn test_delete_forward_end_string() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 9, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
#[test]
fn test_delete_backward_combining_diacritic() {
// U+0301: combining acute accent. 2 bytes.
let (mut ime, statechan, _actionchan) = set_up("abcdefghi\u{0301}", 11, 11);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
#[test]
fn test_delete_forward_combining_diacritic() {
// U+0301: combining acute accent. 2 bytes.
let (mut ime, statechan, _actionchan) = set_up("abcdefghi\u{0301}jkl", 8, 8);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghjkl", state.text);
assert_eq!(8, state.selection.base);
assert_eq!(8, state.selection.extent);
}
#[test]
fn test_delete_backward_emoji() {
// Emoji with a color modifier.
let text = "abcdefghi👦🏻";
let len = text.len() as i64;
let (mut ime, statechan, _actionchan) = set_up(text, len, len);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
#[test]
fn test_delete_forward_emoji() {
// Emoji with a color modifier.
let text = "abcdefghi👦🏻";
let (mut ime, statechan, _actionchan) = set_up(text, 9, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
/// Flags are more complicated because they consist of two REGIONAL INDICATOR SYMBOL LETTERs.
#[test]
fn test_delete_backward_flag() {
// French flag
let text = "abcdefghi🇫🇷";
let len = text.len() as i64;
let (mut ime, statechan, _actionchan) = set_up(text, len, len);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
#[test]
fn test_delete_forward_flag() {
// French flag
let text = "abcdefghi🇫🇷";
let (mut ime, statechan, _actionchan) = set_up(text, 9, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_DELETE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
#[test]
fn test_delete_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 3, 6);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcghi", state.text);
assert_eq!(3, state.selection.base);
assert_eq!(3, state.selection.extent);
}
#[test]
fn test_delete_selection_inverted() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 6, 3);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcghi", state.text);
assert_eq!(3, state.selection.base);
assert_eq!(3, state.selection.extent);
}
#[test]
fn test_delete_no_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", -1, -1);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefghi", state.text);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_delete_with_zero_width_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 3, 3);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abdefghi", state.text);
assert_eq!(2, state.selection.base);
assert_eq!(2, state.selection.extent);
}
#[test]
fn test_delete_with_zero_width_selection_at_end() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 9, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefgh", state.text);
assert_eq!(8, state.selection.base);
assert_eq!(8, state.selection.extent);
}
#[test]
fn test_delete_selection_out_of_bounds() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 20, 24);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abcdefgh", state.text);
assert_eq!(8, state.selection.base);
assert_eq!(8, state.selection.extent);
}
#[test]
fn test_cursor_left_on_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 1, 5);
simulate_keypress(&mut ime, HID_USAGE_KEY_RIGHT, true, true); // right with shift
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!(1, state.selection.base);
assert_eq!(6, state.selection.extent);
simulate_keypress(&mut ime, HID_USAGE_KEY_LEFT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!(1, state.selection.base);
assert_eq!(1, state.selection.extent);
simulate_keypress(&mut ime, HID_USAGE_KEY_LEFT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(4, state.revision);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
simulate_keypress(&mut ime, HID_USAGE_KEY_LEFT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(5, state.revision);
assert_eq!(0, state.selection.base);
assert_eq!(0, state.selection.extent);
}
#[test]
fn test_cursor_left_on_inverted_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 6, 3);
simulate_keypress(&mut ime, HID_USAGE_KEY_LEFT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!(3, state.selection.base);
assert_eq!(3, state.selection.extent);
}
#[test]
fn test_cursor_right_on_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdefghi", 3, 9);
simulate_keypress(&mut ime, HID_USAGE_KEY_LEFT, true, true); // left with shift
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!(3, state.selection.base);
assert_eq!(8, state.selection.extent);
simulate_keypress(&mut ime, HID_USAGE_KEY_RIGHT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!(8, state.selection.base);
assert_eq!(8, state.selection.extent);
simulate_keypress(&mut ime, HID_USAGE_KEY_RIGHT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(4, state.revision);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
simulate_keypress(&mut ime, HID_USAGE_KEY_RIGHT, true, false);
let state = statechan.try_recv().unwrap();
assert_eq!(5, state.revision);
assert_eq!(9, state.selection.base);
assert_eq!(9, state.selection.extent);
}
#[test]
fn test_type_empty_string() {
let (mut ime, statechan, _actionchan) = set_up("", 0, 0);
simulate_keypress(&mut ime, 'a', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("a", state.text);
assert_eq!(1, state.selection.base);
assert_eq!(1, state.selection.extent);
simulate_keypress(&mut ime, 'b', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!("ab", state.text);
assert_eq!(2, state.selection.base);
assert_eq!(2, state.selection.extent);
}
#[test]
fn test_type_at_beginning() {
let (mut ime, statechan, _actionchan) = set_up("cde", 0, 0);
simulate_keypress(&mut ime, 'a', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("acde", state.text);
assert_eq!(1, state.selection.base);
assert_eq!(1, state.selection.extent);
simulate_keypress(&mut ime, 'b', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!("abcde", state.text);
assert_eq!(2, state.selection.base);
assert_eq!(2, state.selection.extent);
}
#[test]
fn test_type_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdef", 2, 5);
simulate_keypress(&mut ime, 'x', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abxf", state.text);
assert_eq!(3, state.selection.base);
assert_eq!(3, state.selection.extent);
}
#[test]
fn test_type_inverted_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdef", 5, 2);
simulate_keypress(&mut ime, 'x', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("abxf", state.text);
assert_eq!(3, state.selection.base);
assert_eq!(3, state.selection.extent);
}
#[test]
fn test_type_invalid_selection() {
let (mut ime, statechan, _actionchan) = set_up("abcdef", -10, 1);
simulate_keypress(&mut ime, 'x', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("xbcdef", state.text);
assert_eq!(1, state.selection.base);
assert_eq!(1, state.selection.extent);
}
#[test]
fn test_set_state() {
let (mut ime, statechan, _actionchan) = set_up("abcdef", 1, 1);
let mut override_state = default_state();
override_state.text = "meow?".to_string();
override_state.selection.base = 4;
override_state.selection.extent = 5;
ime.set_state(override_state);
simulate_keypress(&mut ime, '!', false, false);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("meow!", state.text);
assert_eq!(5, state.selection.base);
assert_eq!(5, state.selection.extent);
}
#[test]
fn test_action() {
let (mut ime, statechan, actionchan) = set_up("abcdef", 1, 1);
simulate_keypress(&mut ime, HID_USAGE_KEY_ENTER, true, false);
assert!(statechan.try_recv().is_err()); // assert did not update state
assert!(actionchan.try_recv().is_ok()); // assert DID send action
}
#[test]
fn test_unicode_selection() {
let (mut ime, statechan, _actionchan) = set_up("m😸eow", 1, 1);
simulate_keypress(&mut ime, HID_USAGE_KEY_RIGHT, true, true);
assert!(statechan.try_recv().is_ok());
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, true);
let state = statechan.try_recv().unwrap();
assert_eq!(3, state.revision);
assert_eq!("meow", state.text);
assert_eq!(1, state.selection.base);
assert_eq!(1, state.selection.extent);
}
#[test]
fn test_unicode_backspace() {
let base: i64 = "m😸".len() as i64;
let (mut ime, statechan, _actionchan) = set_up("m😸eow", base, base);
simulate_keypress(&mut ime, HID_USAGE_KEY_BACKSPACE, true, true);
let state = statechan.try_recv().unwrap();
assert_eq!(2, state.revision);
assert_eq!("meow", state.text);
assert_eq!(1, state.selection.base);
assert_eq!(1, state.selection.extent);
}
}