use inset_embedder::{
FontWeight, Matrix4, Offset, Rect, Size, TargetPlatform, TextAlign, TextDirection, ViewId,
ViewRef,
};
use inset_foundation::{App, Handle, HandleId, Listener, Task};
pub use inset_embedder::{
AutofillConfiguration, FloatingCursorDragState, RawFloatingCursorPoint, SmartDashesType,
SmartQuotesType, TextCapitalization, TextEditingValue, TextInputAction, TextInputConfiguration,
TextInputType,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SelectionChangedCause {
Tap,
DoubleTap,
LongPress,
ForcePress,
Keyboard,
Toolbar,
Drag,
StylusHandwriting,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SelectionRect {
pub position: i32,
pub bounds: Rect,
pub direction: TextDirection,
}
impl SelectionRect {
pub const fn new(position: i32, bounds: Rect, direction: TextDirection) -> SelectionRect {
SelectionRect {
position,
bounds,
direction,
}
}
}
impl std::fmt::Display for SelectionRect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SelectionRect({}, {:?})", self.position, self.bounds)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextInputStyle {
pub font_family: Option<String>,
pub font_size: Option<f64>,
pub font_weight: Option<FontWeight>,
pub text_direction: TextDirection,
pub text_align: TextAlign,
pub letter_spacing: Option<f64>,
pub word_spacing: Option<f64>,
pub line_height: Option<f64>,
}
impl TextInputStyle {
pub fn new(text_direction: TextDirection, text_align: TextAlign) -> TextInputStyle {
TextInputStyle {
font_family: None,
font_size: None,
font_weight: None,
text_direction,
text_align,
letter_spacing: None,
word_spacing: None,
line_height: None,
}
}
}
pub trait TextInputClient: Sized + 'static {
fn current_text_editing_value(self: Handle<Self>, app: &App) -> Option<TextEditingValue>;
fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue);
fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction);
fn perform_private_command(self: Handle<Self>, app: &mut App, action: &str) {
let _ = (self, app, action);
}
fn update_floating_cursor(self: Handle<Self>, app: &mut App, point: RawFloatingCursorPoint) {
let _ = (self, app, point);
}
fn show_autocorrection_prompt_rect(self: Handle<Self>, app: &mut App, start: i32, end: i32) {
let _ = (self, app, start, end);
}
fn connection_closed(self: Handle<Self>, app: &mut App);
fn show_toolbar(self: Handle<Self>, app: &mut App) {
let _ = (self, app);
}
fn insert_text_placeholder(self: Handle<Self>, app: &mut App, size: Size) {
let _ = (self, app, size);
}
fn remove_text_placeholder(self: Handle<Self>, app: &mut App) {
let _ = (self, app);
}
fn perform_selector(self: Handle<Self>, app: &mut App, selector_name: &str) {
let _ = (self, app, selector_name);
}
fn as_text_input_client(self: Handle<Self>) -> AnyTextInputClient {
AnyTextInputClient {
id: self.id(),
vtable: const { &TextInputClientVTable::of::<Self>() },
}
}
}
struct TextInputClientVTable {
current_text_editing_value: fn(&App, HandleId) -> Option<TextEditingValue>,
update_editing_value: fn(&mut App, HandleId, TextEditingValue),
perform_action: fn(&mut App, HandleId, TextInputAction),
perform_private_command: fn(&mut App, HandleId, &str),
update_floating_cursor: fn(&mut App, HandleId, RawFloatingCursorPoint),
show_autocorrection_prompt_rect: fn(&mut App, HandleId, i32, i32),
connection_closed: fn(&mut App, HandleId),
show_toolbar: fn(&mut App, HandleId),
insert_text_placeholder: fn(&mut App, HandleId, Size),
remove_text_placeholder: fn(&mut App, HandleId),
perform_selector: fn(&mut App, HandleId, &str),
}
fn resolve<T: 'static>(id: HandleId) -> Handle<T> {
Handle::from_id(id)
}
impl TextInputClientVTable {
const fn of<C: TextInputClient>() -> TextInputClientVTable {
TextInputClientVTable {
current_text_editing_value: |app, id| C::current_text_editing_value(resolve(id), app),
update_editing_value: |app, id, value| C::update_editing_value(resolve(id), app, value),
perform_action: |app, id, action| C::perform_action(resolve(id), app, action),
perform_private_command: |app, id, action| {
C::perform_private_command(resolve(id), app, action)
},
update_floating_cursor: |app, id, point| {
C::update_floating_cursor(resolve(id), app, point)
},
show_autocorrection_prompt_rect: |app, id, start, end| {
C::show_autocorrection_prompt_rect(resolve(id), app, start, end)
},
connection_closed: |app, id| C::connection_closed(resolve(id), app),
show_toolbar: |app, id| C::show_toolbar(resolve(id), app),
insert_text_placeholder: |app, id, size| {
C::insert_text_placeholder(resolve(id), app, size)
},
remove_text_placeholder: |app, id| C::remove_text_placeholder(resolve(id), app),
perform_selector: |app, id, selector_name| {
C::perform_selector(resolve(id), app, selector_name)
},
}
}
}
#[derive(Clone, Copy)]
pub struct AnyTextInputClient {
id: HandleId,
vtable: &'static TextInputClientVTable,
}
impl AnyTextInputClient {
pub fn current_text_editing_value(self, app: &App) -> Option<TextEditingValue> {
(self.vtable.current_text_editing_value)(app, self.id)
}
pub fn update_editing_value(self, app: &mut App, value: TextEditingValue) {
(self.vtable.update_editing_value)(app, self.id, value);
}
pub fn perform_action(self, app: &mut App, action: TextInputAction) {
(self.vtable.perform_action)(app, self.id, action);
}
pub fn perform_private_command(self, app: &mut App, action: &str) {
(self.vtable.perform_private_command)(app, self.id, action);
}
pub fn update_floating_cursor(self, app: &mut App, point: RawFloatingCursorPoint) {
(self.vtable.update_floating_cursor)(app, self.id, point);
}
pub fn show_autocorrection_prompt_rect(self, app: &mut App, start: i32, end: i32) {
(self.vtable.show_autocorrection_prompt_rect)(app, self.id, start, end);
}
pub fn connection_closed(self, app: &mut App) {
(self.vtable.connection_closed)(app, self.id);
}
pub fn show_toolbar(self, app: &mut App) {
(self.vtable.show_toolbar)(app, self.id);
}
pub fn insert_text_placeholder(self, app: &mut App, size: Size) {
(self.vtable.insert_text_placeholder)(app, self.id, size);
}
pub fn remove_text_placeholder(self, app: &mut App) {
(self.vtable.remove_text_placeholder)(app, self.id);
}
pub fn perform_selector(self, app: &mut App, selector_name: &str) {
(self.vtable.perform_selector)(app, self.id, selector_name);
}
}
impl PartialEq for AnyTextInputClient {
fn eq(&self, other: &AnyTextInputClient) -> bool {
self.id == other.id
}
}
impl Eq for AnyTextInputClient {}
pub trait TextSelectionDelegate: Sized + 'static {
fn text_editing_value(self: Handle<Self>, app: &App) -> TextEditingValue;
fn user_update_text_editing_value(
self: Handle<Self>,
app: &mut App,
value: TextEditingValue,
cause: SelectionChangedCause,
);
fn hide_toolbar(self: Handle<Self>, app: &mut App, hide_handles: bool);
fn bring_into_view(self: Handle<Self>, app: &mut App, position: inset_embedder::TextPosition);
fn cut_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn copy_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn paste_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn select_all_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn look_up_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn search_web_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn share_enabled(self: Handle<Self>, _app: &App) -> bool {
true
}
fn live_text_input_enabled(self: Handle<Self>, _app: &App) -> bool {
false
}
fn cut_selection(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
fn paste_text(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause) -> Task<()>;
fn select_all(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
fn copy_selection(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
fn as_text_selection_delegate(self: Handle<Self>) -> AnyTextSelectionDelegate {
AnyTextSelectionDelegate {
id: self.id(),
vtable: const { &TextSelectionDelegateVTable::of::<Self>() },
}
}
}
struct TextSelectionDelegateVTable {
text_editing_value: fn(&App, HandleId) -> TextEditingValue,
user_update_text_editing_value: fn(&mut App, HandleId, TextEditingValue, SelectionChangedCause),
hide_toolbar: fn(&mut App, HandleId, bool),
bring_into_view: fn(&mut App, HandleId, inset_embedder::TextPosition),
cut_enabled: fn(&App, HandleId) -> bool,
copy_enabled: fn(&App, HandleId) -> bool,
paste_enabled: fn(&App, HandleId) -> bool,
select_all_enabled: fn(&App, HandleId) -> bool,
look_up_enabled: fn(&App, HandleId) -> bool,
search_web_enabled: fn(&App, HandleId) -> bool,
share_enabled: fn(&App, HandleId) -> bool,
live_text_input_enabled: fn(&App, HandleId) -> bool,
cut_selection: fn(&mut App, HandleId, SelectionChangedCause),
paste_text: fn(&mut App, HandleId, SelectionChangedCause) -> Task<()>,
select_all: fn(&mut App, HandleId, SelectionChangedCause),
copy_selection: fn(&mut App, HandleId, SelectionChangedCause),
}
impl TextSelectionDelegateVTable {
const fn of<D: TextSelectionDelegate>() -> TextSelectionDelegateVTable {
TextSelectionDelegateVTable {
text_editing_value: |app, id| D::text_editing_value(resolve(id), app),
user_update_text_editing_value: |app, id, value, cause| {
D::user_update_text_editing_value(resolve(id), app, value, cause)
},
hide_toolbar: |app, id, hide_handles| D::hide_toolbar(resolve(id), app, hide_handles),
bring_into_view: |app, id, position| D::bring_into_view(resolve(id), app, position),
cut_enabled: |app, id| D::cut_enabled(resolve(id), app),
copy_enabled: |app, id| D::copy_enabled(resolve(id), app),
paste_enabled: |app, id| D::paste_enabled(resolve(id), app),
select_all_enabled: |app, id| D::select_all_enabled(resolve(id), app),
look_up_enabled: |app, id| D::look_up_enabled(resolve(id), app),
search_web_enabled: |app, id| D::search_web_enabled(resolve(id), app),
share_enabled: |app, id| D::share_enabled(resolve(id), app),
live_text_input_enabled: |app, id| D::live_text_input_enabled(resolve(id), app),
cut_selection: |app, id, cause| D::cut_selection(resolve(id), app, cause),
paste_text: |app, id, cause| D::paste_text(resolve(id), app, cause),
select_all: |app, id, cause| D::select_all(resolve(id), app, cause),
copy_selection: |app, id, cause| D::copy_selection(resolve(id), app, cause),
}
}
}
#[derive(Clone, Copy)]
pub struct AnyTextSelectionDelegate {
id: HandleId,
vtable: &'static TextSelectionDelegateVTable,
}
impl AnyTextSelectionDelegate {
pub fn text_editing_value(self, app: &App) -> TextEditingValue {
(self.vtable.text_editing_value)(app, self.id)
}
pub fn user_update_text_editing_value(
self,
app: &mut App,
value: TextEditingValue,
cause: SelectionChangedCause,
) {
(self.vtable.user_update_text_editing_value)(app, self.id, value, cause);
}
pub fn hide_toolbar(self, app: &mut App, hide_handles: bool) {
(self.vtable.hide_toolbar)(app, self.id, hide_handles);
}
pub fn bring_into_view(self, app: &mut App, position: inset_embedder::TextPosition) {
(self.vtable.bring_into_view)(app, self.id, position);
}
pub fn cut_enabled(self, app: &App) -> bool {
(self.vtable.cut_enabled)(app, self.id)
}
pub fn copy_enabled(self, app: &App) -> bool {
(self.vtable.copy_enabled)(app, self.id)
}
pub fn paste_enabled(self, app: &App) -> bool {
(self.vtable.paste_enabled)(app, self.id)
}
pub fn select_all_enabled(self, app: &App) -> bool {
(self.vtable.select_all_enabled)(app, self.id)
}
pub fn look_up_enabled(self, app: &App) -> bool {
(self.vtable.look_up_enabled)(app, self.id)
}
pub fn search_web_enabled(self, app: &App) -> bool {
(self.vtable.search_web_enabled)(app, self.id)
}
pub fn share_enabled(self, app: &App) -> bool {
(self.vtable.share_enabled)(app, self.id)
}
pub fn live_text_input_enabled(self, app: &App) -> bool {
(self.vtable.live_text_input_enabled)(app, self.id)
}
pub fn cut_selection(self, app: &mut App, cause: SelectionChangedCause) {
(self.vtable.cut_selection)(app, self.id, cause);
}
pub fn paste_text(self, app: &mut App, cause: SelectionChangedCause) -> Task<()> {
(self.vtable.paste_text)(app, self.id, cause)
}
pub fn select_all(self, app: &mut App, cause: SelectionChangedCause) {
(self.vtable.select_all)(app, self.id, cause);
}
pub fn copy_selection(self, app: &mut App, cause: SelectionChangedCause) {
(self.vtable.copy_selection)(app, self.id, cause);
}
}
impl PartialEq for AnyTextSelectionDelegate {
fn eq(&self, other: &AnyTextSelectionDelegate) -> bool {
self.id == other.id
}
}
impl Eq for AnyTextSelectionDelegate {}
pub struct TextInputConnection {
#[allow(dead_code)]
id: i32,
client: AnyTextInputClient,
cached_size: Option<Size>,
cached_transform: Option<Matrix4>,
cached_rect: Option<Rect>,
cached_caret_rect: Option<Rect>,
cached_selection_rects: Vec<SelectionRect>,
}
impl TextInputConnection {
pub fn attached(self: Handle<Self>, app: &mut App) -> bool {
let input = TextInput::instance(app);
app.get(input)
.current_connection
.is_some_and(|current| current == self)
}
pub fn show(self: Handle<Self>, app: &mut App) {
debug_assert!(self.attached(app));
TextInput::instance(app).show(app);
}
pub fn update_config(self: Handle<Self>, app: &mut App, configuration: TextInputConfiguration) {
debug_assert!(self.attached(app));
TextInput::instance(app).update_config(app, configuration);
}
pub fn set_editing_state(self: Handle<Self>, app: &mut App, value: TextEditingValue) {
debug_assert!(self.attached(app));
TextInput::instance(app).set_editing_state(app, value);
}
pub fn set_editable_size_and_transform(
self: Handle<Self>,
app: &mut App,
editable_box_size: Size,
transform: Matrix4,
) {
let unchanged = {
let connection = app.get(self);
connection.cached_size == Some(editable_box_size)
&& connection.cached_transform.as_ref() == Some(&transform)
};
if unchanged {
return;
}
app.get_mut(self).cached_size = Some(editable_box_size);
app.get_mut(self).cached_transform = Some(transform);
TextInput::instance(app).set_editable_size_and_transform(app, editable_box_size, transform);
}
pub fn set_composing_rect(self: Handle<Self>, app: &mut App, rect: Rect) {
if app.get(self).cached_rect == Some(rect) {
return;
}
app.get_mut(self).cached_rect = Some(rect);
let valid_rect = if rect.is_finite() {
rect
} else {
Offset::ZERO & Size::new(-1.0, -1.0)
};
TextInput::instance(app).set_composing_text_rect(app, valid_rect);
}
pub fn set_caret_rect(self: Handle<Self>, app: &mut App, rect: Rect) {
if app.get(self).cached_caret_rect == Some(rect) {
return;
}
app.get_mut(self).cached_caret_rect = Some(rect);
let valid_rect = if rect.is_finite() {
rect
} else {
Offset::ZERO & Size::new(-1.0, -1.0)
};
TextInput::instance(app).set_caret_rect(app, valid_rect);
}
pub fn set_selection_rects(
self: Handle<Self>,
app: &mut App,
selection_rects: Vec<SelectionRect>,
) {
if app.get(self).cached_selection_rects == selection_rects {
return;
}
app.get_mut(self).cached_selection_rects = selection_rects;
}
pub fn update_style(self: Handle<Self>, app: &mut App, style: TextInputStyle) {
debug_assert!(self.attached(app));
let _ = style;
}
pub fn close(self: Handle<Self>, app: &mut App) {
if self.attached(app) {
TextInput::instance(app).clear_client(app);
}
debug_assert!(!self.attached(app));
}
pub fn connection_closed_received(self: Handle<Self>, app: &mut App) {
let input = TextInput::instance(app);
app.get_mut(input).current_connection = None;
debug_assert!(!self.attached(app));
}
}
pub struct TextInput {
current_connection: Option<Handle<TextInputConnection>>,
current_configuration: Option<TextInputConfiguration>,
#[allow(dead_code)]
last_connection: Option<Handle<TextInputConnection>>,
next_id: i32,
hide_pending: bool,
}
impl Default for TextInput {
fn default() -> TextInput {
TextInput {
current_connection: None,
current_configuration: None,
last_connection: None,
next_id: 1,
hide_pending: false,
}
}
}
impl TextInput {
pub fn instance(app: &mut App) -> Handle<TextInput> {
app.singleton::<TextInput>()
}
pub fn current_connection(
self: Handle<Self>,
app: &App,
) -> Option<Handle<TextInputConnection>> {
app.get(self).current_connection
}
pub fn ensure_initialized(app: &mut App) {
let _ = Self::instance(app);
}
pub fn debug_reset_id(app: &mut App, to: i32) {
if cfg!(debug_assertions) {
let input = Self::instance(app);
app.get_mut(input).next_id = to;
}
}
pub fn attach(
app: &mut App,
client: AnyTextInputClient,
configuration: TextInputConfiguration,
) -> Handle<TextInputConnection> {
let input = Self::instance(app);
let id = {
let input = app.get_mut(input);
let id = input.next_id;
input.next_id += 1;
id
};
let connection = app.create(TextInputConnection {
id,
client,
cached_size: None,
cached_transform: None,
cached_rect: None,
cached_caret_rect: None,
cached_selection_rects: Vec::new(),
});
input.attach_connection(app, connection, configuration);
connection
}
pub fn request_autofill(_app: &mut App) {}
pub fn finish_autofill_context(_app: &mut App, _should_save: bool) {}
fn attach_connection(
self: Handle<Self>,
app: &mut App,
connection: Handle<TextInputConnection>,
configuration: TextInputConfiguration,
) {
debug_ensure_input_action_works_on_platform(app, configuration.input_action);
app.get_mut(self).current_connection = Some(connection);
app.get_mut(self).current_configuration = Some(configuration.clone());
app.get_mut(self).last_connection = Some(connection);
platform_attach(app, &configuration);
}
fn show(self: Handle<Self>, app: &App) {
if let Some(configuration) = app.get(self).current_configuration.clone() {
platform_attach(app, &configuration);
}
}
fn update_config(self: Handle<Self>, app: &mut App, configuration: TextInputConfiguration) {
app.get_mut(self).current_configuration = Some(configuration.clone());
platform_attach(app, &configuration);
}
fn set_editing_state(self: Handle<Self>, app: &App, value: TextEditingValue) {
if let Some(view) = self.target_view(app)
&& let Some(text_input) = view.host().text_input()
{
text_input.set_editing_state(&value);
}
}
fn set_editable_size_and_transform(
self: Handle<Self>,
app: &App,
editable_box_size: Size,
transform: Matrix4,
) {
if let Some(view) = self.target_view(app)
&& let Some(text_input) = view.host().text_input()
{
text_input.set_client_geometry(editable_box_size, &transform);
}
}
fn set_composing_text_rect(self: Handle<Self>, app: &App, rect: Rect) {
if let Some(view) = self.target_view(app)
&& let Some(text_input) = view.host().text_input()
{
text_input.set_composing_rect(rect);
}
}
fn set_caret_rect(self: Handle<Self>, app: &App, rect: Rect) {
if let Some(view) = self.target_view(app)
&& let Some(text_input) = view.host().text_input()
{
text_input.set_caret_rect(rect);
}
}
fn clear_client(self: Handle<Self>, app: &mut App) {
platform_detach(app, self);
app.get_mut(self).current_connection = None;
self.schedule_hide(app);
}
fn hide(self: Handle<Self>, app: &App) {
platform_detach(app, self);
}
fn schedule_hide(self: Handle<Self>, app: &mut App) {
if app.get(self).hide_pending {
return;
}
app.get_mut(self).hide_pending = true;
app.schedule_microtask(Listener::new(move |app| {
app.get_mut(self).hide_pending = false;
if app.get(self).current_connection.is_none() {
self.hide(app);
}
}));
}
fn target_view(self: Handle<Self>, app: &App) -> Option<ViewRef> {
let configuration = app.get(self).current_configuration.as_ref()?;
target_view(app, configuration)
}
pub fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue) {
let Some(connection) = app.get(self).current_connection else {
return;
};
let client = app.get(connection).client;
client.update_editing_value(app, value);
}
pub fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction) {
let Some(connection) = app.get(self).current_connection else {
return;
};
let client = app.get(connection).client;
client.perform_action(app, action);
}
pub fn connection_closed(self: Handle<Self>, app: &mut App) {
let Some(connection) = app.get(self).current_connection else {
return;
};
let client = app.get(connection).client;
client.connection_closed(app);
let _ = connection;
}
}
fn target_view(app: &App, configuration: &TextInputConfiguration) -> Option<ViewRef> {
let id = configuration.view_id?;
app.platform().view(ViewId(id as u64))
}
fn platform_attach(app: &App, configuration: &TextInputConfiguration) {
if let Some(view) = target_view(app, configuration)
&& let Some(text_input) = view.host().text_input()
{
text_input.start(configuration);
}
}
fn platform_detach(app: &App, input: Handle<TextInput>) {
if let Some(view) = input.target_view(app)
&& let Some(text_input) = view.host().text_input()
{
text_input.stop();
}
}
const ANDROID_SUPPORTED_INPUT_ACTIONS: &[TextInputAction] = &[
TextInputAction::None,
TextInputAction::Unspecified,
TextInputAction::Done,
TextInputAction::Send,
TextInputAction::Go,
TextInputAction::Search,
TextInputAction::Next,
TextInputAction::Previous,
TextInputAction::Newline,
];
const IOS_SUPPORTED_INPUT_ACTIONS: &[TextInputAction] = &[
TextInputAction::Unspecified,
TextInputAction::Done,
TextInputAction::Send,
TextInputAction::Go,
TextInputAction::Search,
TextInputAction::Next,
TextInputAction::Newline,
TextInputAction::ContinueAction,
TextInputAction::Join,
TextInputAction::Route,
TextInputAction::EmergencyCall,
];
fn debug_ensure_input_action_works_on_platform(app: &App, input_action: TextInputAction) {
if !cfg!(debug_assertions) {
return;
}
match app.platform().target_platform() {
TargetPlatform::IOS => {
debug_assert!(
IOS_SUPPORTED_INPUT_ACTIONS.contains(&input_action),
"The requested TextInputAction \"{input_action:?}\" is not supported on iOS."
);
}
TargetPlatform::Android => {
debug_assert!(
ANDROID_SUPPORTED_INPUT_ACTIONS.contains(&input_action),
"The requested TextInputAction \"{input_action:?}\" is not supported on Android."
);
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use inset_embedder::TextAffinity;
use inset_foundation::AppCell;
use super::*;
use crate::text_editing::TextSelection;
#[derive(Default)]
struct RecordingClient {
value: TextEditingValue,
actions: Vec<TextInputAction>,
closed: bool,
}
impl TextInputClient for RecordingClient {
fn current_text_editing_value(self: Handle<Self>, app: &App) -> Option<TextEditingValue> {
Some(app.get(self).value.clone())
}
fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue) {
app.get_mut(self).value = value;
}
fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction) {
app.get_mut(self).actions.push(action);
}
fn connection_closed(self: Handle<Self>, app: &mut App) {
app.get_mut(self).closed = true;
}
}
#[test]
fn attach_then_host_value_reaches_the_client() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let client = app.create(RecordingClient::default());
let connection = TextInput::attach(
&mut app,
client.as_text_input_client(),
TextInputConfiguration::default(),
);
assert!(connection.attached(&mut app));
TextInput::instance(&mut app).update_editing_value(
&mut app,
TextEditingValue::new()
.text("hi")
.selection(TextSelection::collapsed(2, TextAffinity::Downstream)),
);
assert_eq!(client.current_text_editing_value(&app).unwrap().text, "hi");
}
#[test]
fn close_schedules_hide_and_detaches() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let client = app.create(RecordingClient::default());
let connection = TextInput::attach(
&mut app,
client.as_text_input_client(),
TextInputConfiguration::default(),
);
connection.close(&mut app);
assert!(!connection.attached(&mut app));
app.drain_microtasks();
}
#[test]
fn connection_closed_from_the_host_notifies_the_client() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let client = app.create(RecordingClient::default());
TextInput::attach(
&mut app,
client.as_text_input_client(),
TextInputConfiguration::default(),
);
TextInput::instance(&mut app).connection_closed(&mut app);
assert!(app.get(client).closed);
}
#[test]
fn perform_action_reaches_the_client() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let client = app.create(RecordingClient::default());
TextInput::attach(
&mut app,
client.as_text_input_client(),
TextInputConfiguration::builder()
.input_action(TextInputAction::Done)
.build(),
);
TextInput::instance(&mut app).perform_action(&mut app, TextInputAction::Done);
assert_eq!(app.get(client).actions, [TextInputAction::Done]);
}
#[test]
fn composing_rect_that_is_not_finite_sends_negative_size() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let client = app.create(RecordingClient::default());
let connection = TextInput::attach(
&mut app,
client.as_text_input_client(),
TextInputConfiguration::default(),
);
let invalid = Rect::from_ltrb(f64::NAN, 0.0, 1.0, 1.0);
connection.set_composing_rect(&mut app, invalid);
assert!(!app.get(connection).cached_rect.unwrap().is_finite());
}
#[test]
fn selection_rect_prints_like_dart() {
let rect = SelectionRect::new(3, Rect::from_ltwh(1.0, 2.0, 3.0, 4.0), TextDirection::Ltr);
assert_eq!(
rect.to_string(),
format!("SelectionRect(3, {:?})", rect.bounds)
);
}
}