use super::super::text_edit_buffer::test_support::{TestClient, TestInputMethod};
use super::tests::{info, input_method, HostCall};
use super::*;
use ferroui_base::data::TemplateBinding;
use ferroui_base::input::text_input::ITextInputMethodImpl;
use ferroui_base::Ref;
use ferroui_controls::presenters::ContentPresenter;
use ferroui_controls::templates::{FuncControlTemplate, FuncTemplateNameScopeExtensions};
use ferroui_controls::testing::{MockWindowingPlatform, TestServices, UnitTestApplication, UnitTestApplicationScope};
use ferroui_controls::{Border, ContentControl, Control, Window};
use InputScopeNameValue as Scope;
struct NoInputMethod;
impl ITextInputMethodImpl for NoInputMethod {
fn set_client(&self, _client: Option<Rc<dyn TextInputMethodClient>>) {}
fn set_cursor_rect(&self, _rect: Rect) {}
fn set_options(&self, _options: &TextInputOptions) {}
fn reset(&self) {}
}
struct Owner {
element: Ref<Border>,
window: Ref<Window>,
_app: UnitTestApplicationScope,
}
impl Owner {
fn new(client: &Rc<TestClient>) -> Self {
let app = UnitTestApplication::start(TestServices::focusable_window().with_windowing_platform(
MockWindowingPlatform::with_window_impl(|| {
let window = MockWindowingPlatform::create_window_mock();
window.setup_feature::<dyn ITextInputMethodImpl>(Rc::new(NoInputMethod));
window
}),
));
let element = Border::new();
element.set_focusable(true);
element.set_height(20.0);
let offered = client.clone();
element.text_input_method_client_requested(move |_, e| e.set_client(Some(offered.clone())));
let window = Window::new();
window.set_template(Some(FuncControlTemplate::for_type::<Window>(|_, scope| {
let presenter = ContentPresenter::new();
presenter.set_name(Some("PART_ContentPresenter".to_owned()));
let content = ContentControl::content_property().as_property();
presenter.bind_binding(content, &TemplateBinding::new(content));
presenter.register_in_name_scope(&**scope).upcast()
})));
window.set_content(Some(Control::boxed(element.clone())));
window.show();
assert!(element.focus());
Self { element, window, _app: app }
}
fn set_scopes(&self, names: &[InputScopeNameValue]) {
TextInputOptions::set_input_scope_names(&self.element, Some(Rc::from(names)));
}
}
impl Drop for Owner {
fn drop(&mut self) {
self.window.close();
}
}
fn connection_info(options: &TextInputOptions, scope: Option<InputScopeNameValue>) -> EditorInfo {
let mut out_attrs = EditorInfo::default();
AndroidInputMethod::fill_connection_editor_info(options, scope, &mut out_attrs, 36);
out_attrs
}
#[test]
fn a_legacy_client_keeps_upstreams_editor_info_and_never_restarts_for_its_options() {
use input_types::*;
let (host, input_method) = input_method();
let client = TestClient::new("abc", 1, 1);
input_method.set_options(&TextInputOptions::default());
input_method.set_client(Some(client.clone()));
assert!(client.input_method_connection().is_none());
host.create_input_connection().unwrap();
host.take_calls();
for content_type in [TextInputContentType::Email, TextInputContentType::Password, TextInputContentType::Pin] {
let options = TextInputOptions { content_type, ..Default::default() };
input_method.set_options(&options);
assert_eq!(host.take_calls(), []);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor, info(&options));
}
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, CLASS_NUMBER | NUMBER_VARIATION_PASSWORD);
input_method.set_options(&TextInputOptions { content_type: TextInputContentType::Password, ..Default::default() });
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 0x81);
assert_eq!(editor.ime_options & ime_flags::NO_PERSONALIZED_LEARNING, 0);
}
#[test]
fn a_connection_client_gets_the_keyboard_of_the_first_scope_and_restarts_only_when_it_changes() {
let (host, input_method) = input_method();
let client = TestClient::editing("", 0, 0);
let owner = Owner::new(&client);
assert!(client.input_method_connection().is_some_and(|connection| connection.is_current()));
let options = TextInputOptions { content_type: TextInputContentType::Digits, ..Default::default() };
input_method.set_options(&options);
input_method.set_client(Some(client.clone()));
assert_eq!(host.take_calls(), [HostCall::RequestFocus, HostCall::RestartInput, HostCall::ShowSoftInput]);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 3);
owner.set_scopes(&[Scope::Digits, Scope::TelephoneNumber]);
input_method.set_options(&options);
assert_eq!(host.take_calls(), [HostCall::RestartInput]);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 2);
input_method.set_options(&options);
owner.set_scopes(&[Scope::Digits]);
input_method.set_options(&options);
assert_eq!(host.take_calls(), []);
owner.set_scopes(&[Scope::Default, Scope::Digits]);
input_method.set_options(&options);
assert_eq!(host.take_calls(), [HostCall::RestartInput]);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 1);
owner.set_scopes(&[]);
input_method.set_options(&options);
assert_eq!(host.take_calls(), [HostCall::RestartInput]);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 3);
input_method.set_client(Some(client.clone()));
host.take_calls();
owner.set_scopes(&[Scope::Url]);
input_method.set_options(&options);
assert_eq!(host.take_calls(), []);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 0x11);
}
#[test]
fn a_legacy_client_after_a_connection_client_does_not_restart_for_its_options() {
let (host, input_method) = input_method();
let editing = TestClient::editing("", 0, 0);
let owner = Owner::new(&editing);
input_method.set_options(&TextInputOptions::default());
input_method.set_client(Some(editing.clone()));
host.create_input_connection().unwrap();
owner.set_scopes(&[Scope::EmailSmtpAddress]);
let legacy = TestClient::new("abc", 0, 0);
input_method.set_client(Some(legacy));
host.take_calls();
input_method.set_options(&TextInputOptions { content_type: TextInputContentType::Url, ..Default::default() });
assert_eq!(host.take_calls(), []);
let (_, editor) = host.create_input_connection().unwrap();
assert_eq!(editor.input_type, 0x11);
}
#[test]
fn a_restart_for_changed_options_keeps_the_composing_region_of_the_same_client() {
let (host, input_method) = input_method();
let client = TestClient::editing("abcd", 1, 3);
let owner = Owner::new(&client);
let top_level: Rc<dyn IInputConnectionTopLevel> = TestInputMethod::new(Some(client.clone()));
input_method.set_options(&TextInputOptions::default());
input_method.set_client(Some(client.clone()));
let init = host.init.borrow().clone().unwrap();
let first = init(&top_level, &mut EditorInfo::default()).unwrap();
assert!(first.set_composing_text(Some("x"), 1));
assert_eq!(client.text(), "axd");
assert_eq!(first.edit_buffer().composition(), Some(TextSelection::new(1, 2)));
host.take_calls();
owner.set_scopes(&[Scope::EmailSmtpAddress]);
input_method.set_options(&TextInputOptions::default());
assert_eq!(host.take_calls(), [HostCall::RestartInput]);
first.close_connection();
let init = host.init.borrow().clone().unwrap();
let next = init(&top_level, &mut EditorInfo::default()).unwrap();
assert!(!Rc::ptr_eq(&first, &next));
assert_eq!(next.edit_buffer().composition(), Some(TextSelection::new(1, 2)));
assert!(next.set_composing_text(Some("yz"), 1));
assert_eq!(client.text(), "ayzd");
assert!(next.commit_text(Some("accepted"), 1));
assert_eq!(client.text(), "aacceptedd");
assert_eq!(next.edit_buffer().composition(), None);
}
#[test]
fn a_client_assignment_cancels_the_composing_region_of_a_restart() {
for same_client in [false, true] {
let (host, input_method) = input_method();
let client = TestClient::editing("abcd", 1, 3);
let owner = Owner::new(&client);
input_method.set_options(&TextInputOptions::default());
input_method.set_client(Some(client.clone()));
let (first, _) = host.create_input_connection().unwrap();
assert!(first.set_composing_region(1, 3));
owner.set_scopes(&[Scope::EmailSmtpAddress]);
input_method.set_options(&TextInputOptions::default());
input_method.set_client(Some(if same_client { client.clone() } else { TestClient::editing("else", 0, 0) }));
owner.set_scopes(&[Scope::Search]);
input_method.set_options(&TextInputOptions::default());
let (next, _) = host.create_input_connection().unwrap();
assert_eq!(next.edit_buffer().composition(), None);
}
}
#[test]
fn input_scope_distinguishes_numeric_components_phones_and_alphanumeric_pins() {
let cases = [
(Scope::Digits, 0x2),
(Scope::DateMonthNumber, 0x2),
(Scope::DateDayNumber, 0x2),
(Scope::DateYear, 0x2),
(Scope::TimeHour, 0x2),
(Scope::TimeMinutesOrSeconds, 0x2),
(Scope::TelephoneNumber, 0x3),
(Scope::TelephoneCountryCode, 0x3),
(Scope::TelephoneAreaCode, 0x3),
(Scope::TelephoneLocalNumber, 0x3),
(Scope::Number, 0x3002),
(Scope::CurrencyAmount, 0x3002),
(Scope::FormulaNumber, 0x3002),
(Scope::EmailSmtpAddress, 0x21),
(Scope::EmailNameOrAddress, 0x21),
(Scope::PersonalFullName, 0x61),
(Scope::NameOrPhoneNumber, 0x61),
(Scope::NumericPassword, 0x12),
(Scope::NumericPin, 0x12),
(Scope::Password, 0x80081),
(Scope::AlphanumericPin, 0x80081),
(Scope::Hiragana, 0x1),
];
let options = TextInputOptions { content_type: TextInputContentType::Url, ..Default::default() };
for (scope, expected) in cases {
assert_eq!(connection_info(&options, Some(scope)).input_type, expected, "{scope:?}");
}
}
#[test]
fn without_a_scope_a_connection_client_differs_from_upstream_only_for_passwords_and_pins() {
use input_types::*;
for content_type in [
TextInputContentType::Normal,
TextInputContentType::Alpha,
TextInputContentType::Digits,
TextInputContentType::Number,
TextInputContentType::Email,
TextInputContentType::Url,
TextInputContentType::Name,
TextInputContentType::Social,
TextInputContentType::Search,
] {
for (auto_capitalization, multiline, show_suggestions, is_sensitive) in
[(false, false, None, false), (true, true, Some(false), false), (false, true, Some(true), true)]
{
let options = TextInputOptions {
content_type,
auto_capitalization,
multiline,
show_suggestions,
is_sensitive,
..Default::default()
};
assert_eq!(connection_info(&options, None), info(&options), "{options:?}");
}
}
let password = TextInputOptions { content_type: TextInputContentType::Password, ..Default::default() };
assert_eq!(info(&password).input_type, 0x81);
let editor = connection_info(&password, None);
assert_eq!(editor.input_type, CLASS_TEXT | TEXT_VARIATION_PASSWORD | TEXT_FLAG_NO_SUGGESTIONS);
assert_ne!(editor.ime_options & ime_flags::NO_PERSONALIZED_LEARNING, 0);
let pin = TextInputOptions { content_type: TextInputContentType::Pin, ..Default::default() };
let editor = connection_info(&pin, None);
assert_eq!(editor.input_type, 0x12);
assert_ne!(editor.ime_options & ime_flags::NO_PERSONALIZED_LEARNING, 0);
}
#[test]
fn input_scope_numeric_pads_do_not_get_text_flags_and_secrets_disable_learning() {
for scope in [Scope::Digits, Scope::NumericPin] {
let options = TextInputOptions {
auto_capitalization: true,
multiline: true,
show_suggestions: Some(true),
..Default::default()
};
let editor = connection_info(&options, Some(scope));
assert_eq!(editor.input_type, if scope == Scope::Digits { 2 } else { 0x12 });
assert_eq!(editor.initial_caps_mode, 0);
assert_eq!(editor.ime_options & ime_flags::NO_PERSONALIZED_LEARNING != 0, scope == Scope::NumericPin);
let mut old = EditorInfo::default();
AndroidInputMethod::fill_connection_editor_info(&options, Some(Scope::AlphanumericPin), &mut old, 25);
assert_eq!(old.ime_options & ime_flags::NO_PERSONALIZED_LEARNING, 0);
assert_ne!(old.input_type & input_types::TEXT_FLAG_NO_SUGGESTIONS, 0);
}
}
#[test]
fn input_scope_override_preserves_independently_configured_privacy() {
use input_types::*;
for (content_type, is_sensitive) in [
(TextInputContentType::Password, false),
(TextInputContentType::Pin, false),
(TextInputContentType::Normal, true),
] {
for (scope, input_type) in [
(Scope::Default, CLASS_TEXT | TEXT_FLAG_NO_SUGGESTIONS),
(Scope::EmailSmtpAddress, CLASS_TEXT | TEXT_VARIATION_EMAIL_ADDRESS | TEXT_FLAG_NO_SUGGESTIONS),
(Scope::Digits, CLASS_NUMBER),
(Scope::TelephoneNumber, CLASS_PHONE),
] {
let options = TextInputOptions { content_type, is_sensitive, show_suggestions: Some(true), ..Default::default() };
let editor = connection_info(&options, Some(scope));
assert_eq!(editor.input_type, input_type, "{content_type:?}/{is_sensitive}/{scope:?}");
assert_ne!(editor.ime_options & ime_flags::NO_PERSONALIZED_LEARNING, 0);
let mut old = EditorInfo::default();
AndroidInputMethod::fill_connection_editor_info(&options, Some(scope), &mut old, 25);
assert_eq!(old.input_type, input_type);
assert_eq!(old.ime_options & ime_flags::NO_PERSONALIZED_LEARNING, 0);
}
}
}