use std::fmt;
use crate::text_editing::TextSelection;
use crate::{Brightness, Locale, Offset, TextAffinity, TextPosition, TextRange};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SmartDashesType {
Disabled,
Enabled,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SmartQuotesType {
Disabled,
Enabled,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TextInputType {
pub index: i32,
pub signed: Option<bool>,
pub decimal: Option<bool>,
pub password: Option<bool>,
}
impl TextInputType {
const fn named(index: i32) -> TextInputType {
TextInputType {
index,
signed: None,
decimal: None,
password: None,
}
}
pub const TEXT: TextInputType = TextInputType::named(0);
pub const MULTILINE: TextInputType = TextInputType::named(1);
pub const NUMBER: TextInputType = TextInputType::number_with_options();
pub const PHONE: TextInputType = TextInputType::named(3);
pub const DATETIME: TextInputType = TextInputType::named(4);
pub const EMAIL_ADDRESS: TextInputType = TextInputType::named(5);
pub const URL: TextInputType = TextInputType::named(6);
pub const VISIBLE_PASSWORD: TextInputType = TextInputType::named(7);
pub const NAME: TextInputType = TextInputType::named(8);
pub const STREET_ADDRESS: TextInputType = TextInputType::named(9);
pub const NONE: TextInputType = TextInputType::named(10);
pub const WEB_SEARCH: TextInputType = TextInputType::named(11);
pub const TWITTER: TextInputType = TextInputType::named(12);
pub const VALUES: &[TextInputType] = &[
Self::TEXT,
Self::MULTILINE,
Self::NUMBER,
Self::PHONE,
Self::DATETIME,
Self::EMAIL_ADDRESS,
Self::URL,
Self::VISIBLE_PASSWORD,
Self::NAME,
Self::STREET_ADDRESS,
Self::NONE,
Self::WEB_SEARCH,
Self::TWITTER,
];
const NAMES: &[&str] = &[
"text",
"multiline",
"number",
"phone",
"datetime",
"emailAddress",
"url",
"visiblePassword",
"name",
"address",
"none",
"webSearch",
"twitter",
];
pub const fn number_with_options() -> TextInputType {
TextInputType {
index: 2,
signed: Some(false),
decimal: Some(false),
password: Some(false),
}
}
pub fn signed(mut self, signed: bool) -> TextInputType {
self.signed = Some(signed);
self
}
pub fn decimal(mut self, decimal: bool) -> TextInputType {
self.decimal = Some(decimal);
self
}
pub fn password(mut self, password: bool) -> TextInputType {
self.password = Some(password);
self
}
fn name(self) -> String {
format!("TextInputType.{}", Self::NAMES[self.index as usize])
}
}
impl fmt::Display for TextInputType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TextInputType(name: {}, signed: {:?}, decimal: {:?}, password: {:?})",
self.name(),
self.signed,
self.decimal,
self.password
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextInputAction {
None,
Unspecified,
Done,
Go,
Search,
Send,
Next,
Previous,
ContinueAction,
Join,
Route,
EmergencyCall,
Newline,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextCapitalization {
Words,
Sentences,
Characters,
None,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AutofillConfiguration {
pub enabled: bool,
pub unique_identifier: String,
pub autofill_hints: Vec<String>,
pub current_editing_value: TextEditingValue,
pub hint_text: Option<String>,
}
impl AutofillConfiguration {
pub const DISABLED: AutofillConfiguration = AutofillConfiguration {
enabled: false,
unique_identifier: String::new(),
autofill_hints: Vec::new(),
current_editing_value: TextEditingValue::EMPTY,
hint_text: None,
};
pub fn new(
unique_identifier: impl Into<String>,
autofill_hints: Vec<String>,
current_editing_value: TextEditingValue,
) -> AutofillConfiguration {
AutofillConfiguration {
enabled: true,
unique_identifier: unique_identifier.into(),
autofill_hints,
current_editing_value,
hint_text: None,
}
}
pub fn hint_text(mut self, hint_text: impl Into<String>) -> AutofillConfiguration {
self.hint_text = Some(hint_text.into());
self
}
}
impl fmt::Display for AutofillConfiguration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AutofillConfiguration(enabled: {}, uniqueIdentifier: {}, autofillHints: {:?}, currentEditingValue: {}",
self.enabled, self.unique_identifier, self.autofill_hints, self.current_editing_value
)?;
if let Some(hint_text) = &self.hint_text {
write!(f, ", hintText: {hint_text}")?;
}
write!(f, ")")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextInputConfiguration {
pub view_id: Option<i32>,
pub input_type: TextInputType,
pub read_only: bool,
pub obscure_text: bool,
pub autocorrect: bool,
pub smart_dashes_type: SmartDashesType,
pub smart_quotes_type: SmartQuotesType,
pub enable_suggestions: bool,
pub enable_interactive_selection: bool,
pub action_label: Option<String>,
pub input_action: TextInputAction,
pub text_capitalization: TextCapitalization,
pub keyboard_appearance: Brightness,
pub autofill_configuration: AutofillConfiguration,
pub enable_ime_personalized_learning: bool,
pub allowed_mime_types: Vec<String>,
pub enable_delta_model: bool,
pub hint_locales: Vec<Locale>,
pub enable_inline_prediction: Option<bool>,
}
impl TextInputConfiguration {
pub fn builder() -> TextInputConfigurationBuilder {
TextInputConfigurationBuilder {
configuration: TextInputConfiguration {
view_id: None,
input_type: TextInputType::TEXT,
read_only: false,
obscure_text: false,
autocorrect: true,
smart_dashes_type: SmartDashesType::Enabled,
smart_quotes_type: SmartQuotesType::Enabled,
enable_suggestions: true,
enable_interactive_selection: true,
action_label: None,
input_action: TextInputAction::Done,
text_capitalization: TextCapitalization::None,
keyboard_appearance: Brightness::Light,
autofill_configuration: AutofillConfiguration::DISABLED,
enable_ime_personalized_learning: true,
allowed_mime_types: Vec::new(),
enable_delta_model: false,
hint_locales: Vec::new(),
enable_inline_prediction: None,
},
smart_dashes_type: None,
smart_quotes_type: None,
}
}
pub fn copy_with(&self) -> TextInputConfigurationBuilder {
TextInputConfigurationBuilder {
configuration: self.clone(),
smart_dashes_type: Some(self.smart_dashes_type),
smart_quotes_type: Some(self.smart_quotes_type),
}
}
}
#[derive(Clone, Debug)]
pub struct TextInputConfigurationBuilder {
configuration: TextInputConfiguration,
smart_dashes_type: Option<SmartDashesType>,
smart_quotes_type: Option<SmartQuotesType>,
}
impl TextInputConfigurationBuilder {
pub fn view_id(mut self, view_id: i32) -> TextInputConfigurationBuilder {
self.configuration.view_id = Some(view_id);
self
}
pub fn input_type(mut self, input_type: TextInputType) -> TextInputConfigurationBuilder {
self.configuration.input_type = input_type;
self
}
pub fn read_only(mut self, read_only: bool) -> TextInputConfigurationBuilder {
self.configuration.read_only = read_only;
self
}
pub fn obscure_text(mut self, obscure_text: bool) -> TextInputConfigurationBuilder {
self.configuration.obscure_text = obscure_text;
self
}
pub fn autocorrect(mut self, autocorrect: bool) -> TextInputConfigurationBuilder {
self.configuration.autocorrect = autocorrect;
self
}
pub fn smart_dashes_type(
mut self,
smart_dashes_type: SmartDashesType,
) -> TextInputConfigurationBuilder {
self.smart_dashes_type = Some(smart_dashes_type);
self
}
pub fn smart_quotes_type(
mut self,
smart_quotes_type: SmartQuotesType,
) -> TextInputConfigurationBuilder {
self.smart_quotes_type = Some(smart_quotes_type);
self
}
pub fn enable_suggestions(mut self, enable_suggestions: bool) -> TextInputConfigurationBuilder {
self.configuration.enable_suggestions = enable_suggestions;
self
}
pub fn enable_interactive_selection(
mut self,
enable_interactive_selection: bool,
) -> TextInputConfigurationBuilder {
self.configuration.enable_interactive_selection = enable_interactive_selection;
self
}
pub fn action_label(
mut self,
action_label: impl Into<String>,
) -> TextInputConfigurationBuilder {
self.configuration.action_label = Some(action_label.into());
self
}
pub fn input_action(mut self, input_action: TextInputAction) -> TextInputConfigurationBuilder {
self.configuration.input_action = input_action;
self
}
pub fn keyboard_appearance(
mut self,
keyboard_appearance: Brightness,
) -> TextInputConfigurationBuilder {
self.configuration.keyboard_appearance = keyboard_appearance;
self
}
pub fn text_capitalization(
mut self,
text_capitalization: TextCapitalization,
) -> TextInputConfigurationBuilder {
self.configuration.text_capitalization = text_capitalization;
self
}
pub fn autofill_configuration(
mut self,
autofill_configuration: AutofillConfiguration,
) -> TextInputConfigurationBuilder {
self.configuration.autofill_configuration = autofill_configuration;
self
}
pub fn enable_ime_personalized_learning(
mut self,
enable_ime_personalized_learning: bool,
) -> TextInputConfigurationBuilder {
self.configuration.enable_ime_personalized_learning = enable_ime_personalized_learning;
self
}
pub fn allowed_mime_types(
mut self,
allowed_mime_types: Vec<String>,
) -> TextInputConfigurationBuilder {
self.configuration.allowed_mime_types = allowed_mime_types;
self
}
pub fn enable_delta_model(mut self, enable_delta_model: bool) -> TextInputConfigurationBuilder {
self.configuration.enable_delta_model = enable_delta_model;
self
}
pub fn hint_locales(mut self, hint_locales: Vec<Locale>) -> TextInputConfigurationBuilder {
self.configuration.hint_locales = hint_locales;
self
}
pub fn enable_inline_prediction(
mut self,
enable_inline_prediction: bool,
) -> TextInputConfigurationBuilder {
self.configuration.enable_inline_prediction = Some(enable_inline_prediction);
self
}
pub fn build(self) -> TextInputConfiguration {
let mut configuration = self.configuration;
let obscure_text = configuration.obscure_text;
configuration.smart_dashes_type = self.smart_dashes_type.unwrap_or(if obscure_text {
SmartDashesType::Disabled
} else {
SmartDashesType::Enabled
});
configuration.smart_quotes_type = self.smart_quotes_type.unwrap_or(if obscure_text {
SmartQuotesType::Disabled
} else {
SmartQuotesType::Enabled
});
configuration
}
}
impl Default for TextInputConfiguration {
fn default() -> TextInputConfiguration {
TextInputConfiguration::builder().build()
}
}
impl fmt::Display for TextInputConfiguration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = vec![
format!("inputType: {}", self.input_type),
format!("readOnly: {}", self.read_only),
format!("obscureText: {}", self.obscure_text),
format!("autocorrect: {}", self.autocorrect),
format!("smartDashesType: {:?}", self.smart_dashes_type),
format!("smartQuotesType: {:?}", self.smart_quotes_type),
format!("enableSuggestions: {}", self.enable_suggestions),
format!(
"enableInteractiveSelection: {}",
self.enable_interactive_selection
),
format!("inputAction: {:?}", self.input_action),
format!("keyboardAppearance: {:?}", self.keyboard_appearance),
format!("textCapitalization: {:?}", self.text_capitalization),
format!("autofillConfiguration: {}", self.autofill_configuration),
format!(
"enableIMEPersonalizedLearning: {}",
self.enable_ime_personalized_learning
),
format!("allowedMimeTypes: {:?}", self.allowed_mime_types),
format!("enableDeltaModel: {}", self.enable_delta_model),
];
if let Some(view_id) = self.view_id {
parts.insert(0, format!("viewId: {view_id}"));
}
if let Some(action_label) = &self.action_label {
parts.insert(
parts
.iter()
.position(|p| p.starts_with("inputAction:"))
.unwrap_or(parts.len()),
format!("actionLabel: {action_label}"),
);
}
if !self.hint_locales.is_empty() {
parts.push(format!("hintLocales: {:?}", self.hint_locales));
}
if let Some(enable_inline_prediction) = self.enable_inline_prediction {
parts.push(format!(
"enableInlinePrediction: {enable_inline_prediction}"
));
}
write!(f, "TextInputConfiguration({})", parts.join(", "))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FloatingCursorDragState {
Start,
Update,
End,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RawFloatingCursorPoint {
pub offset: Option<Offset>,
pub start_location: Option<(Offset, TextPosition)>,
pub state: FloatingCursorDragState,
}
impl RawFloatingCursorPoint {
pub fn new(state: FloatingCursorDragState) -> RawFloatingCursorPoint {
let point = RawFloatingCursorPoint {
offset: None,
start_location: None,
state,
};
if state == FloatingCursorDragState::Update {
point
} else {
point.assemble()
}
}
pub fn offset(mut self, offset: Offset) -> RawFloatingCursorPoint {
self.offset = Some(offset);
self.assemble()
}
pub fn start_location(
mut self,
start_location: (Offset, TextPosition),
) -> RawFloatingCursorPoint {
self.start_location = Some(start_location);
self
}
fn assemble(self) -> RawFloatingCursorPoint {
debug_assert!(
self.state != FloatingCursorDragState::Update || self.offset.is_some(),
"offset must not be null if the state is Update"
);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TextEditingValue {
pub text: String,
pub selection: TextSelection,
pub composing: TextRange,
}
impl TextEditingValue {
pub const EMPTY: TextEditingValue = TextEditingValue {
text: String::new(),
selection: TextSelection::collapsed(-1, TextAffinity::Downstream),
composing: TextRange::EMPTY,
};
pub fn new() -> TextEditingValue {
TextEditingValue::EMPTY
}
pub fn text(mut self, text: impl Into<String>) -> TextEditingValue {
self.text = text.into();
self
}
pub fn selection(mut self, selection: TextSelection) -> TextEditingValue {
self.selection = selection;
self
}
pub fn composing(mut self, composing: TextRange) -> TextEditingValue {
self.composing = composing;
self
}
pub fn copy_with(&self) -> TextEditingValue {
self.clone()
}
pub fn is_composing_range_valid(&self) -> bool {
self.composing.is_valid()
&& self.composing.is_normalized()
&& self.composing.end <= utf16_len(&self.text)
}
pub fn replaced(
&self,
replacement_range: impl Into<TextRange>,
replacement_string: &str,
) -> TextEditingValue {
let replacement_range = replacement_range.into();
if !replacement_range.is_valid() {
return self.clone();
}
debug_assert!(replacement_range.is_normalized());
let new_text = format!(
"{}{}{}",
replacement_range.text_before(&self.text),
replacement_string,
replacement_range.text_after(&self.text),
);
if replacement_range.end - replacement_range.start == utf16_len(replacement_string) {
return self.copy_with().text(new_text);
}
let replacement_len = utf16_len(replacement_string);
let adjust_index = |original_index: i32| {
let replaced_length = if original_index <= replacement_range.start
&& original_index < replacement_range.end
{
0
} else {
replacement_len
};
let removed_length = original_index
.clamp(replacement_range.start, replacement_range.end)
- replacement_range.start;
original_index + replaced_length - removed_length
};
let adjusted_selection = TextSelection::new(
adjust_index(self.selection.base_offset),
adjust_index(self.selection.extent_offset),
);
let adjusted_composing = TextRange::new(
adjust_index(self.composing.start),
adjust_index(self.composing.end),
);
debug_assert!(text_range_is_valid(adjusted_selection.range(), &new_text));
debug_assert!(text_range_is_valid(adjusted_composing, &new_text));
TextEditingValue::new()
.text(new_text)
.selection(adjusted_selection)
.composing(adjusted_composing)
}
}
impl Default for TextEditingValue {
fn default() -> TextEditingValue {
TextEditingValue::new()
}
}
impl fmt::Display for TextEditingValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TextEditingValue(text: \u{2524}{}\u{251C}, selection: {}, composing: {:?})",
self.text, self.selection, self.composing
)
}
}
fn utf16_len(text: &str) -> i32 {
text.encode_utf16().count() as i32
}
fn text_range_is_valid(range: TextRange, text: &str) -> bool {
if range.start == -1 && range.end == -1 {
return true;
}
let len = utf16_len(text);
debug_assert!(
range.start >= 0 && range.start <= len,
"Range start {} is out of text of length {len}",
range.start
);
debug_assert!(
range.end >= 0 && range.end <= len,
"Range end {} is out of text of length {len}",
range.end
);
true
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_TEXT: &str = "From a false proposition, anything follows.";
#[test]
fn replaced_deletes_a_selection() {
let selection = TextSelection::new(5, 13);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(selection, ""),
TextEditingValue::new()
.text("From proposition, anything follows.")
.selection(TextSelection::collapsed(5, TextAffinity::Downstream))
);
}
#[test]
fn replaced_deletes_a_reversed_selection() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(selection, ""),
TextEditingValue::new()
.text("From proposition, anything follows.")
.selection(TextSelection::collapsed(5, TextAffinity::Downstream))
);
}
#[test]
fn replaced_inserts_at_the_caret() {
let selection = TextSelection::collapsed(5, TextAffinity::Downstream);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(selection, "AA"),
TextEditingValue::new()
.text("From AAa false proposition, anything follows.")
.selection(TextSelection::collapsed(7, TextAffinity::Downstream))
);
}
#[test]
fn replaced_before_selection_shifts_the_range() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(4, 5), "AA"),
TextEditingValue::new()
.text("FromAAa false proposition, anything follows.")
.selection(TextSelection::new(14, 6))
);
}
#[test]
fn replaced_after_selection_leaves_the_range() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(13, 14), "AA"),
TextEditingValue::new()
.text("From a false AAroposition, anything follows.")
.selection(selection)
);
}
#[test]
fn replaced_inside_selection_start_boundary() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(5, 6), "AA"),
TextEditingValue::new()
.text("From AA false proposition, anything follows.")
.selection(TextSelection::new(14, 5))
);
}
#[test]
fn replaced_inside_selection_end_boundary() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(12, 13), "AA"),
TextEditingValue::new()
.text("From a falseAAproposition, anything follows.")
.selection(TextSelection::new(14, 5))
);
}
#[test]
fn replaced_deletes_after_selection() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(13, 14), ""),
TextEditingValue::new()
.text("From a false roposition, anything follows.")
.selection(selection)
);
}
#[test]
fn replaced_deletes_inside_selection_start_boundary() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(5, 6), ""),
TextEditingValue::new()
.text("From false proposition, anything follows.")
.selection(TextSelection::new(12, 5))
);
}
#[test]
fn replaced_deletes_inside_selection_end_boundary() {
let selection = TextSelection::new(13, 5);
assert_eq!(
TextEditingValue::new()
.text(TEST_TEXT)
.selection(selection)
.replaced(TextRange::new(12, 13), ""),
TextEditingValue::new()
.text("From a falseproposition, anything follows.")
.selection(TextSelection::new(12, 5))
);
}
#[test]
fn empty_prints_like_dart() {
assert_eq!(
TextEditingValue::EMPTY.to_string(),
format!(
"TextEditingValue(text: \u{2524}\u{251C}, selection: {}, composing: {:?})",
TextSelection::collapsed(-1, TextAffinity::Downstream),
TextRange::EMPTY
)
);
}
#[test]
fn number_with_options_defaults_match_dart() {
assert_eq!(TextInputType::NUMBER.signed, Some(false));
assert_eq!(
TextInputType::number_with_options()
.signed(true)
.decimal(true),
TextInputType {
index: 2,
signed: Some(true),
decimal: Some(true),
password: Some(false),
}
);
}
#[test]
fn obscure_text_disables_smart_punctuation_unless_overridden() {
let obscured = TextInputConfiguration::builder().obscure_text(true).build();
assert!(obscured.obscure_text);
assert_eq!(obscured.smart_dashes_type, SmartDashesType::Disabled);
assert_eq!(obscured.smart_quotes_type, SmartQuotesType::Disabled);
let given_first = TextInputConfiguration::builder()
.smart_dashes_type(SmartDashesType::Enabled)
.obscure_text(true)
.build();
assert_eq!(given_first.smart_dashes_type, SmartDashesType::Enabled);
}
#[test]
fn a_copy_keeps_the_smart_punctuation_it_was_made_with() {
let copy = TextInputConfiguration::default()
.copy_with()
.obscure_text(true)
.build();
assert_eq!(copy.smart_dashes_type, SmartDashesType::Enabled);
assert_eq!(copy.smart_quotes_type, SmartQuotesType::Enabled);
}
#[test]
fn autofill_disabled_is_the_configuration_default() {
let configuration = TextInputConfiguration::default();
assert!(!configuration.autofill_configuration.enabled);
assert_eq!(
AutofillConfiguration::new("id", vec!["email".into()], TextEditingValue::EMPTY)
.hint_text("email")
.hint_text,
Some("email".into())
);
}
}