1use inset_embedder::{
8 FontWeight, Matrix4, Offset, Rect, Size, TargetPlatform, TextAlign, TextDirection, ViewId,
9 ViewRef,
10};
11use inset_foundation::{App, Handle, HandleId, Listener, Task};
12
13pub use inset_embedder::{
14 AutofillConfiguration, FloatingCursorDragState, RawFloatingCursorPoint, SmartDashesType,
15 SmartQuotesType, TextCapitalization, TextEditingValue, TextInputAction, TextInputConfiguration,
16 TextInputType,
17};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum SelectionChangedCause {
23 Tap,
26
27 DoubleTap,
30
31 LongPress,
34
35 ForcePress,
38
39 Keyboard,
45
46 Toolbar,
51
52 Drag,
55
56 StylusHandwriting,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct SelectionRect {
69 pub position: i32,
71 pub bounds: Rect,
74 pub direction: TextDirection,
76}
77
78impl SelectionRect {
79 pub const fn new(position: i32, bounds: Rect, direction: TextDirection) -> SelectionRect {
82 SelectionRect {
83 position,
84 bounds,
85 direction,
86 }
87 }
88}
89
90impl std::fmt::Display for SelectionRect {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(f, "SelectionRect({}, {:?})", self.position, self.bounds)
93 }
94}
95
96#[derive(Clone, Debug, PartialEq)]
103pub struct TextInputStyle {
104 pub font_family: Option<String>,
106 pub font_size: Option<f64>,
108 pub font_weight: Option<FontWeight>,
110 pub text_direction: TextDirection,
112 pub text_align: TextAlign,
114 pub letter_spacing: Option<f64>,
116 pub word_spacing: Option<f64>,
119 pub line_height: Option<f64>,
121}
122
123impl TextInputStyle {
124 pub fn new(text_direction: TextDirection, text_align: TextAlign) -> TextInputStyle {
126 TextInputStyle {
127 font_family: None,
128 font_size: None,
129 font_weight: None,
130 text_direction,
131 text_align,
132 letter_spacing: None,
133 word_spacing: None,
134 line_height: None,
135 }
136 }
137}
138
139pub trait TextInputClient: Sized + 'static {
146 fn current_text_editing_value(self: Handle<Self>, app: &App) -> Option<TextEditingValue>;
148
149 fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue);
154
155 fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction);
157
158 fn perform_private_command(self: Handle<Self>, app: &mut App, action: &str) {
161 let _ = (self, app, action);
162 }
163
164 fn update_floating_cursor(self: Handle<Self>, app: &mut App, point: RawFloatingCursorPoint) {
166 let _ = (self, app, point);
167 }
168
169 fn show_autocorrection_prompt_rect(self: Handle<Self>, app: &mut App, start: i32, end: i32) {
172 let _ = (self, app, start, end);
173 }
174
175 fn connection_closed(self: Handle<Self>, app: &mut App);
179
180 fn show_toolbar(self: Handle<Self>, app: &mut App) {
184 let _ = (self, app);
185 }
186
187 fn insert_text_placeholder(self: Handle<Self>, app: &mut App, size: Size) {
190 let _ = (self, app, size);
191 }
192
193 fn remove_text_placeholder(self: Handle<Self>, app: &mut App) {
195 let _ = (self, app);
196 }
197
198 fn perform_selector(self: Handle<Self>, app: &mut App, selector_name: &str) {
202 let _ = (self, app, selector_name);
203 }
204
205 fn as_text_input_client(self: Handle<Self>) -> AnyTextInputClient {
207 AnyTextInputClient {
208 id: self.id(),
209 vtable: const { &TextInputClientVTable::of::<Self>() },
210 }
211 }
212}
213
214struct TextInputClientVTable {
215 current_text_editing_value: fn(&App, HandleId) -> Option<TextEditingValue>,
216 update_editing_value: fn(&mut App, HandleId, TextEditingValue),
217 perform_action: fn(&mut App, HandleId, TextInputAction),
218 perform_private_command: fn(&mut App, HandleId, &str),
219 update_floating_cursor: fn(&mut App, HandleId, RawFloatingCursorPoint),
220 show_autocorrection_prompt_rect: fn(&mut App, HandleId, i32, i32),
221 connection_closed: fn(&mut App, HandleId),
222 show_toolbar: fn(&mut App, HandleId),
223 insert_text_placeholder: fn(&mut App, HandleId, Size),
224 remove_text_placeholder: fn(&mut App, HandleId),
225 perform_selector: fn(&mut App, HandleId, &str),
226}
227
228fn resolve<T: 'static>(id: HandleId) -> Handle<T> {
229 Handle::from_id(id)
230}
231
232impl TextInputClientVTable {
233 const fn of<C: TextInputClient>() -> TextInputClientVTable {
234 TextInputClientVTable {
235 current_text_editing_value: |app, id| C::current_text_editing_value(resolve(id), app),
236 update_editing_value: |app, id, value| C::update_editing_value(resolve(id), app, value),
237 perform_action: |app, id, action| C::perform_action(resolve(id), app, action),
238 perform_private_command: |app, id, action| {
239 C::perform_private_command(resolve(id), app, action)
240 },
241 update_floating_cursor: |app, id, point| {
242 C::update_floating_cursor(resolve(id), app, point)
243 },
244 show_autocorrection_prompt_rect: |app, id, start, end| {
245 C::show_autocorrection_prompt_rect(resolve(id), app, start, end)
246 },
247 connection_closed: |app, id| C::connection_closed(resolve(id), app),
248 show_toolbar: |app, id| C::show_toolbar(resolve(id), app),
249 insert_text_placeholder: |app, id, size| {
250 C::insert_text_placeholder(resolve(id), app, size)
251 },
252 remove_text_placeholder: |app, id| C::remove_text_placeholder(resolve(id), app),
253 perform_selector: |app, id, selector_name| {
254 C::perform_selector(resolve(id), app, selector_name)
255 },
256 }
257 }
258}
259
260#[derive(Clone, Copy)]
262pub struct AnyTextInputClient {
263 id: HandleId,
264 vtable: &'static TextInputClientVTable,
265}
266
267impl AnyTextInputClient {
268 pub fn current_text_editing_value(self, app: &App) -> Option<TextEditingValue> {
269 (self.vtable.current_text_editing_value)(app, self.id)
270 }
271
272 pub fn update_editing_value(self, app: &mut App, value: TextEditingValue) {
273 (self.vtable.update_editing_value)(app, self.id, value);
274 }
275
276 pub fn perform_action(self, app: &mut App, action: TextInputAction) {
277 (self.vtable.perform_action)(app, self.id, action);
278 }
279
280 pub fn perform_private_command(self, app: &mut App, action: &str) {
281 (self.vtable.perform_private_command)(app, self.id, action);
282 }
283
284 pub fn update_floating_cursor(self, app: &mut App, point: RawFloatingCursorPoint) {
285 (self.vtable.update_floating_cursor)(app, self.id, point);
286 }
287
288 pub fn show_autocorrection_prompt_rect(self, app: &mut App, start: i32, end: i32) {
289 (self.vtable.show_autocorrection_prompt_rect)(app, self.id, start, end);
290 }
291
292 pub fn connection_closed(self, app: &mut App) {
293 (self.vtable.connection_closed)(app, self.id);
294 }
295
296 pub fn show_toolbar(self, app: &mut App) {
297 (self.vtable.show_toolbar)(app, self.id);
298 }
299
300 pub fn insert_text_placeholder(self, app: &mut App, size: Size) {
301 (self.vtable.insert_text_placeholder)(app, self.id, size);
302 }
303
304 pub fn remove_text_placeholder(self, app: &mut App) {
305 (self.vtable.remove_text_placeholder)(app, self.id);
306 }
307
308 pub fn perform_selector(self, app: &mut App, selector_name: &str) {
309 (self.vtable.perform_selector)(app, self.id, selector_name);
310 }
311}
312
313impl PartialEq for AnyTextInputClient {
314 fn eq(&self, other: &AnyTextInputClient) -> bool {
315 self.id == other.id
316 }
317}
318
319impl Eq for AnyTextInputClient {}
320
321pub trait TextSelectionDelegate: Sized + 'static {
324 fn text_editing_value(self: Handle<Self>, app: &App) -> TextEditingValue;
326
327 fn user_update_text_editing_value(
333 self: Handle<Self>,
334 app: &mut App,
335 value: TextEditingValue,
336 cause: SelectionChangedCause,
337 );
338
339 fn hide_toolbar(self: Handle<Self>, app: &mut App, hide_handles: bool);
345
346 fn bring_into_view(self: Handle<Self>, app: &mut App, position: inset_embedder::TextPosition);
349
350 fn cut_enabled(self: Handle<Self>, _app: &App) -> bool {
352 true
353 }
354
355 fn copy_enabled(self: Handle<Self>, _app: &App) -> bool {
357 true
358 }
359
360 fn paste_enabled(self: Handle<Self>, _app: &App) -> bool {
362 true
363 }
364
365 fn select_all_enabled(self: Handle<Self>, _app: &App) -> bool {
367 true
368 }
369
370 fn look_up_enabled(self: Handle<Self>, _app: &App) -> bool {
372 true
373 }
374
375 fn search_web_enabled(self: Handle<Self>, _app: &App) -> bool {
377 true
378 }
379
380 fn share_enabled(self: Handle<Self>, _app: &App) -> bool {
382 true
383 }
384
385 fn live_text_input_enabled(self: Handle<Self>, _app: &App) -> bool {
387 false
388 }
389
390 fn cut_selection(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
392
393 fn paste_text(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause) -> Task<()>;
395
396 fn select_all(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
398
399 fn copy_selection(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
401
402 fn as_text_selection_delegate(self: Handle<Self>) -> AnyTextSelectionDelegate {
404 AnyTextSelectionDelegate {
405 id: self.id(),
406 vtable: const { &TextSelectionDelegateVTable::of::<Self>() },
407 }
408 }
409}
410
411struct TextSelectionDelegateVTable {
412 text_editing_value: fn(&App, HandleId) -> TextEditingValue,
413 user_update_text_editing_value: fn(&mut App, HandleId, TextEditingValue, SelectionChangedCause),
414 hide_toolbar: fn(&mut App, HandleId, bool),
415 bring_into_view: fn(&mut App, HandleId, inset_embedder::TextPosition),
416 cut_enabled: fn(&App, HandleId) -> bool,
417 copy_enabled: fn(&App, HandleId) -> bool,
418 paste_enabled: fn(&App, HandleId) -> bool,
419 select_all_enabled: fn(&App, HandleId) -> bool,
420 look_up_enabled: fn(&App, HandleId) -> bool,
421 search_web_enabled: fn(&App, HandleId) -> bool,
422 share_enabled: fn(&App, HandleId) -> bool,
423 live_text_input_enabled: fn(&App, HandleId) -> bool,
424 cut_selection: fn(&mut App, HandleId, SelectionChangedCause),
425 paste_text: fn(&mut App, HandleId, SelectionChangedCause) -> Task<()>,
426 select_all: fn(&mut App, HandleId, SelectionChangedCause),
427 copy_selection: fn(&mut App, HandleId, SelectionChangedCause),
428}
429
430impl TextSelectionDelegateVTable {
431 const fn of<D: TextSelectionDelegate>() -> TextSelectionDelegateVTable {
432 TextSelectionDelegateVTable {
433 text_editing_value: |app, id| D::text_editing_value(resolve(id), app),
434 user_update_text_editing_value: |app, id, value, cause| {
435 D::user_update_text_editing_value(resolve(id), app, value, cause)
436 },
437 hide_toolbar: |app, id, hide_handles| D::hide_toolbar(resolve(id), app, hide_handles),
438 bring_into_view: |app, id, position| D::bring_into_view(resolve(id), app, position),
439 cut_enabled: |app, id| D::cut_enabled(resolve(id), app),
440 copy_enabled: |app, id| D::copy_enabled(resolve(id), app),
441 paste_enabled: |app, id| D::paste_enabled(resolve(id), app),
442 select_all_enabled: |app, id| D::select_all_enabled(resolve(id), app),
443 look_up_enabled: |app, id| D::look_up_enabled(resolve(id), app),
444 search_web_enabled: |app, id| D::search_web_enabled(resolve(id), app),
445 share_enabled: |app, id| D::share_enabled(resolve(id), app),
446 live_text_input_enabled: |app, id| D::live_text_input_enabled(resolve(id), app),
447 cut_selection: |app, id, cause| D::cut_selection(resolve(id), app, cause),
448 paste_text: |app, id, cause| D::paste_text(resolve(id), app, cause),
449 select_all: |app, id, cause| D::select_all(resolve(id), app, cause),
450 copy_selection: |app, id, cause| D::copy_selection(resolve(id), app, cause),
451 }
452 }
453}
454
455#[derive(Clone, Copy)]
457pub struct AnyTextSelectionDelegate {
458 id: HandleId,
459 vtable: &'static TextSelectionDelegateVTable,
460}
461
462impl AnyTextSelectionDelegate {
463 pub fn text_editing_value(self, app: &App) -> TextEditingValue {
464 (self.vtable.text_editing_value)(app, self.id)
465 }
466
467 pub fn user_update_text_editing_value(
468 self,
469 app: &mut App,
470 value: TextEditingValue,
471 cause: SelectionChangedCause,
472 ) {
473 (self.vtable.user_update_text_editing_value)(app, self.id, value, cause);
474 }
475
476 pub fn hide_toolbar(self, app: &mut App, hide_handles: bool) {
477 (self.vtable.hide_toolbar)(app, self.id, hide_handles);
478 }
479
480 pub fn bring_into_view(self, app: &mut App, position: inset_embedder::TextPosition) {
481 (self.vtable.bring_into_view)(app, self.id, position);
482 }
483
484 pub fn cut_enabled(self, app: &App) -> bool {
485 (self.vtable.cut_enabled)(app, self.id)
486 }
487
488 pub fn copy_enabled(self, app: &App) -> bool {
489 (self.vtable.copy_enabled)(app, self.id)
490 }
491
492 pub fn paste_enabled(self, app: &App) -> bool {
493 (self.vtable.paste_enabled)(app, self.id)
494 }
495
496 pub fn select_all_enabled(self, app: &App) -> bool {
497 (self.vtable.select_all_enabled)(app, self.id)
498 }
499
500 pub fn look_up_enabled(self, app: &App) -> bool {
501 (self.vtable.look_up_enabled)(app, self.id)
502 }
503
504 pub fn search_web_enabled(self, app: &App) -> bool {
505 (self.vtable.search_web_enabled)(app, self.id)
506 }
507
508 pub fn share_enabled(self, app: &App) -> bool {
509 (self.vtable.share_enabled)(app, self.id)
510 }
511
512 pub fn live_text_input_enabled(self, app: &App) -> bool {
513 (self.vtable.live_text_input_enabled)(app, self.id)
514 }
515
516 pub fn cut_selection(self, app: &mut App, cause: SelectionChangedCause) {
517 (self.vtable.cut_selection)(app, self.id, cause);
518 }
519
520 pub fn paste_text(self, app: &mut App, cause: SelectionChangedCause) -> Task<()> {
521 (self.vtable.paste_text)(app, self.id, cause)
522 }
523
524 pub fn select_all(self, app: &mut App, cause: SelectionChangedCause) {
525 (self.vtable.select_all)(app, self.id, cause);
526 }
527
528 pub fn copy_selection(self, app: &mut App, cause: SelectionChangedCause) {
529 (self.vtable.copy_selection)(app, self.id, cause);
530 }
531}
532
533impl PartialEq for AnyTextSelectionDelegate {
534 fn eq(&self, other: &AnyTextSelectionDelegate) -> bool {
535 self.id == other.id
536 }
537}
538
539impl Eq for AnyTextSelectionDelegate {}
540
541pub struct TextInputConnection {
548 #[allow(dead_code)]
549 id: i32,
550 client: AnyTextInputClient,
551 cached_size: Option<Size>,
552 cached_transform: Option<Matrix4>,
553 cached_rect: Option<Rect>,
554 cached_caret_rect: Option<Rect>,
555 cached_selection_rects: Vec<SelectionRect>,
556}
557
558impl TextInputConnection {
559 pub fn attached(self: Handle<Self>, app: &mut App) -> bool {
561 let input = TextInput::instance(app);
562 app.get(input)
563 .current_connection
564 .is_some_and(|current| current == self)
565 }
566
567 pub fn show(self: Handle<Self>, app: &mut App) {
569 debug_assert!(self.attached(app));
570 TextInput::instance(app).show(app);
571 }
572
573 pub fn update_config(self: Handle<Self>, app: &mut App, configuration: TextInputConfiguration) {
576 debug_assert!(self.attached(app));
577 TextInput::instance(app).update_config(app, configuration);
578 }
579
580 pub fn set_editing_state(self: Handle<Self>, app: &mut App, value: TextEditingValue) {
583 debug_assert!(self.attached(app));
584 TextInput::instance(app).set_editing_state(app, value);
585 }
586
587 pub fn set_editable_size_and_transform(
594 self: Handle<Self>,
595 app: &mut App,
596 editable_box_size: Size,
597 transform: Matrix4,
598 ) {
599 let unchanged = {
600 let connection = app.get(self);
601 connection.cached_size == Some(editable_box_size)
602 && connection.cached_transform.as_ref() == Some(&transform)
603 };
604 if unchanged {
605 return;
606 }
607 app.get_mut(self).cached_size = Some(editable_box_size);
608 app.get_mut(self).cached_transform = Some(transform);
609 TextInput::instance(app).set_editable_size_and_transform(app, editable_box_size, transform);
610 }
611
612 pub fn set_composing_rect(self: Handle<Self>, app: &mut App, rect: Rect) {
618 if app.get(self).cached_rect == Some(rect) {
619 return;
620 }
621 app.get_mut(self).cached_rect = Some(rect);
622 let valid_rect = if rect.is_finite() {
623 rect
624 } else {
625 Offset::ZERO & Size::new(-1.0, -1.0)
626 };
627 TextInput::instance(app).set_composing_text_rect(app, valid_rect);
628 }
629
630 pub fn set_caret_rect(self: Handle<Self>, app: &mut App, rect: Rect) {
633 if app.get(self).cached_caret_rect == Some(rect) {
634 return;
635 }
636 app.get_mut(self).cached_caret_rect = Some(rect);
637 let valid_rect = if rect.is_finite() {
638 rect
639 } else {
640 Offset::ZERO & Size::new(-1.0, -1.0)
641 };
642 TextInput::instance(app).set_caret_rect(app, valid_rect);
643 }
644
645 pub fn set_selection_rects(
648 self: Handle<Self>,
649 app: &mut App,
650 selection_rects: Vec<SelectionRect>,
651 ) {
652 if app.get(self).cached_selection_rects == selection_rects {
653 return;
654 }
655 app.get_mut(self).cached_selection_rects = selection_rects;
656 }
657
658 pub fn update_style(self: Handle<Self>, app: &mut App, style: TextInputStyle) {
660 debug_assert!(self.attached(app));
661 let _ = style;
662 }
663
664 pub fn close(self: Handle<Self>, app: &mut App) {
669 if self.attached(app) {
670 TextInput::instance(app).clear_client(app);
671 }
672 debug_assert!(!self.attached(app));
673 }
674
675 pub fn connection_closed_received(self: Handle<Self>, app: &mut App) {
679 let input = TextInput::instance(app);
680 app.get_mut(input).current_connection = None;
681 debug_assert!(!self.attached(app));
682 }
683}
684
685pub struct TextInput {
695 current_connection: Option<Handle<TextInputConnection>>,
696 current_configuration: Option<TextInputConfiguration>,
697 #[allow(dead_code)]
698 last_connection: Option<Handle<TextInputConnection>>,
699 next_id: i32,
700 hide_pending: bool,
701}
702
703impl Default for TextInput {
704 fn default() -> TextInput {
705 TextInput {
706 current_connection: None,
707 current_configuration: None,
708 last_connection: None,
709 next_id: 1,
710 hide_pending: false,
711 }
712 }
713}
714
715impl TextInput {
716 pub fn instance(app: &mut App) -> Handle<TextInput> {
718 app.singleton::<TextInput>()
719 }
720
721 pub fn current_connection(
723 self: Handle<Self>,
724 app: &App,
725 ) -> Option<Handle<TextInputConnection>> {
726 app.get(self).current_connection
727 }
728
729 pub fn ensure_initialized(app: &mut App) {
732 let _ = Self::instance(app);
733 }
734
735 pub fn debug_reset_id(app: &mut App, to: i32) {
737 if cfg!(debug_assertions) {
738 let input = Self::instance(app);
739 app.get_mut(input).next_id = to;
740 }
741 }
742
743 pub fn attach(
745 app: &mut App,
746 client: AnyTextInputClient,
747 configuration: TextInputConfiguration,
748 ) -> Handle<TextInputConnection> {
749 let input = Self::instance(app);
750 let id = {
751 let input = app.get_mut(input);
752 let id = input.next_id;
753 input.next_id += 1;
754 id
755 };
756 let connection = app.create(TextInputConnection {
757 id,
758 client,
759 cached_size: None,
760 cached_transform: None,
761 cached_rect: None,
762 cached_caret_rect: None,
763 cached_selection_rects: Vec::new(),
764 });
765 input.attach_connection(app, connection, configuration);
766 connection
767 }
768
769 pub fn request_autofill(_app: &mut App) {}
778
779 pub fn finish_autofill_context(_app: &mut App, _should_save: bool) {}
821
822 fn attach_connection(
823 self: Handle<Self>,
824 app: &mut App,
825 connection: Handle<TextInputConnection>,
826 configuration: TextInputConfiguration,
827 ) {
828 debug_ensure_input_action_works_on_platform(app, configuration.input_action);
829 app.get_mut(self).current_connection = Some(connection);
830 app.get_mut(self).current_configuration = Some(configuration.clone());
831 app.get_mut(self).last_connection = Some(connection);
832 platform_attach(app, &configuration);
833 }
834
835 fn show(self: Handle<Self>, app: &App) {
836 if let Some(configuration) = app.get(self).current_configuration.clone() {
837 platform_attach(app, &configuration);
838 }
839 }
840
841 fn update_config(self: Handle<Self>, app: &mut App, configuration: TextInputConfiguration) {
842 app.get_mut(self).current_configuration = Some(configuration.clone());
843 platform_attach(app, &configuration);
844 }
845
846 fn set_editing_state(self: Handle<Self>, app: &App, value: TextEditingValue) {
847 if let Some(view) = self.target_view(app)
848 && let Some(text_input) = view.host().text_input()
849 {
850 text_input.set_editing_state(&value);
851 }
852 }
853
854 fn set_editable_size_and_transform(
855 self: Handle<Self>,
856 app: &App,
857 editable_box_size: Size,
858 transform: Matrix4,
859 ) {
860 if let Some(view) = self.target_view(app)
861 && let Some(text_input) = view.host().text_input()
862 {
863 text_input.set_client_geometry(editable_box_size, &transform);
864 }
865 }
866
867 fn set_composing_text_rect(self: Handle<Self>, app: &App, rect: Rect) {
868 if let Some(view) = self.target_view(app)
869 && let Some(text_input) = view.host().text_input()
870 {
871 text_input.set_composing_rect(rect);
872 }
873 }
874
875 fn set_caret_rect(self: Handle<Self>, app: &App, rect: Rect) {
876 if let Some(view) = self.target_view(app)
877 && let Some(text_input) = view.host().text_input()
878 {
879 text_input.set_caret_rect(rect);
880 }
881 }
882
883 fn clear_client(self: Handle<Self>, app: &mut App) {
884 platform_detach(app, self);
885 app.get_mut(self).current_connection = None;
886 self.schedule_hide(app);
887 }
888
889 fn hide(self: Handle<Self>, app: &App) {
890 platform_detach(app, self);
891 }
892
893 fn schedule_hide(self: Handle<Self>, app: &mut App) {
894 if app.get(self).hide_pending {
895 return;
896 }
897 app.get_mut(self).hide_pending = true;
898 app.schedule_microtask(Listener::new(move |app| {
899 app.get_mut(self).hide_pending = false;
900 if app.get(self).current_connection.is_none() {
901 self.hide(app);
902 }
903 }));
904 }
905
906 fn target_view(self: Handle<Self>, app: &App) -> Option<ViewRef> {
907 let configuration = app.get(self).current_configuration.as_ref()?;
908 target_view(app, configuration)
909 }
910
911 pub fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue) {
916 let Some(connection) = app.get(self).current_connection else {
917 return;
918 };
919 let client = app.get(connection).client;
920 client.update_editing_value(app, value);
921 }
922
923 pub fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction) {
925 let Some(connection) = app.get(self).current_connection else {
926 return;
927 };
928 let client = app.get(connection).client;
929 client.perform_action(app, action);
930 }
931
932 pub fn connection_closed(self: Handle<Self>, app: &mut App) {
934 let Some(connection) = app.get(self).current_connection else {
935 return;
936 };
937 let client = app.get(connection).client;
938 client.connection_closed(app);
939 let _ = connection;
940 }
941}
942
943fn target_view(app: &App, configuration: &TextInputConfiguration) -> Option<ViewRef> {
946 let id = configuration.view_id?;
947 app.platform().view(ViewId(id as u64))
948}
949
950fn platform_attach(app: &App, configuration: &TextInputConfiguration) {
951 if let Some(view) = target_view(app, configuration)
952 && let Some(text_input) = view.host().text_input()
953 {
954 text_input.start(configuration);
955 }
956}
957
958fn platform_detach(app: &App, input: Handle<TextInput>) {
959 if let Some(view) = input.target_view(app)
960 && let Some(text_input) = view.host().text_input()
961 {
962 text_input.stop();
963 }
964}
965
966const ANDROID_SUPPORTED_INPUT_ACTIONS: &[TextInputAction] = &[
967 TextInputAction::None,
968 TextInputAction::Unspecified,
969 TextInputAction::Done,
970 TextInputAction::Send,
971 TextInputAction::Go,
972 TextInputAction::Search,
973 TextInputAction::Next,
974 TextInputAction::Previous,
975 TextInputAction::Newline,
976];
977
978const IOS_SUPPORTED_INPUT_ACTIONS: &[TextInputAction] = &[
979 TextInputAction::Unspecified,
980 TextInputAction::Done,
981 TextInputAction::Send,
982 TextInputAction::Go,
983 TextInputAction::Search,
984 TextInputAction::Next,
985 TextInputAction::Newline,
986 TextInputAction::ContinueAction,
987 TextInputAction::Join,
988 TextInputAction::Route,
989 TextInputAction::EmergencyCall,
990];
991
992fn debug_ensure_input_action_works_on_platform(app: &App, input_action: TextInputAction) {
993 if !cfg!(debug_assertions) {
994 return;
995 }
996 match app.platform().target_platform() {
997 TargetPlatform::IOS => {
998 debug_assert!(
999 IOS_SUPPORTED_INPUT_ACTIONS.contains(&input_action),
1000 "The requested TextInputAction \"{input_action:?}\" is not supported on iOS."
1001 );
1002 }
1003 TargetPlatform::Android => {
1004 debug_assert!(
1005 ANDROID_SUPPORTED_INPUT_ACTIONS.contains(&input_action),
1006 "The requested TextInputAction \"{input_action:?}\" is not supported on Android."
1007 );
1008 }
1009 _ => {}
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use inset_embedder::TextAffinity;
1016 use inset_foundation::AppCell;
1017
1018 use super::*;
1019 use crate::text_editing::TextSelection;
1020
1021 #[derive(Default)]
1022 struct RecordingClient {
1023 value: TextEditingValue,
1024 actions: Vec<TextInputAction>,
1025 closed: bool,
1026 }
1027
1028 impl TextInputClient for RecordingClient {
1029 fn current_text_editing_value(self: Handle<Self>, app: &App) -> Option<TextEditingValue> {
1030 Some(app.get(self).value.clone())
1031 }
1032
1033 fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue) {
1034 app.get_mut(self).value = value;
1035 }
1036
1037 fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction) {
1038 app.get_mut(self).actions.push(action);
1039 }
1040
1041 fn connection_closed(self: Handle<Self>, app: &mut App) {
1042 app.get_mut(self).closed = true;
1043 }
1044 }
1045
1046 #[test]
1047 fn attach_then_host_value_reaches_the_client() {
1048 let cell = AppCell::new();
1049 let mut app = cell.borrow_mut();
1050 let client = app.create(RecordingClient::default());
1051 let connection = TextInput::attach(
1052 &mut app,
1053 client.as_text_input_client(),
1054 TextInputConfiguration::default(),
1055 );
1056 assert!(connection.attached(&mut app));
1057
1058 TextInput::instance(&mut app).update_editing_value(
1059 &mut app,
1060 TextEditingValue::new()
1061 .text("hi")
1062 .selection(TextSelection::collapsed(2, TextAffinity::Downstream)),
1063 );
1064 assert_eq!(client.current_text_editing_value(&app).unwrap().text, "hi");
1065 }
1066
1067 #[test]
1068 fn close_schedules_hide_and_detaches() {
1069 let cell = AppCell::new();
1070 let mut app = cell.borrow_mut();
1071 let client = app.create(RecordingClient::default());
1072 let connection = TextInput::attach(
1073 &mut app,
1074 client.as_text_input_client(),
1075 TextInputConfiguration::default(),
1076 );
1077 connection.close(&mut app);
1078 assert!(!connection.attached(&mut app));
1079 app.drain_microtasks();
1080 }
1081
1082 #[test]
1083 fn connection_closed_from_the_host_notifies_the_client() {
1084 let cell = AppCell::new();
1085 let mut app = cell.borrow_mut();
1086 let client = app.create(RecordingClient::default());
1087 TextInput::attach(
1088 &mut app,
1089 client.as_text_input_client(),
1090 TextInputConfiguration::default(),
1091 );
1092 TextInput::instance(&mut app).connection_closed(&mut app);
1093 assert!(app.get(client).closed);
1094 }
1095
1096 #[test]
1097 fn perform_action_reaches_the_client() {
1098 let cell = AppCell::new();
1099 let mut app = cell.borrow_mut();
1100 let client = app.create(RecordingClient::default());
1101 TextInput::attach(
1102 &mut app,
1103 client.as_text_input_client(),
1104 TextInputConfiguration::builder()
1105 .input_action(TextInputAction::Done)
1106 .build(),
1107 );
1108 TextInput::instance(&mut app).perform_action(&mut app, TextInputAction::Done);
1109 assert_eq!(app.get(client).actions, [TextInputAction::Done]);
1110 }
1111
1112 #[test]
1113 fn composing_rect_that_is_not_finite_sends_negative_size() {
1114 let cell = AppCell::new();
1115 let mut app = cell.borrow_mut();
1116 let client = app.create(RecordingClient::default());
1117 let connection = TextInput::attach(
1118 &mut app,
1119 client.as_text_input_client(),
1120 TextInputConfiguration::default(),
1121 );
1122 let invalid = Rect::from_ltrb(f64::NAN, 0.0, 1.0, 1.0);
1123 connection.set_composing_rect(&mut app, invalid);
1124 assert!(!app.get(connection).cached_rect.unwrap().is_finite());
1125 }
1126
1127 #[test]
1128 fn selection_rect_prints_like_dart() {
1129 let rect = SelectionRect::new(3, Rect::from_ltwh(1.0, 2.0, 3.0, 4.0), TextDirection::Ltr);
1130 assert_eq!(
1131 rect.to_string(),
1132 format!("SelectionRect(3, {:?})", rect.bounds)
1133 );
1134 }
1135}