Skip to main content

inset_services/
text_input.rs

1//! Flutter counterpart: `services/text_input.dart`.
2//!
3//! The value types live in `inset-embedder` because [`inset_embedder::View`]
4//! and [`inset_embedder::EmbedderClient`] name them. This file is the
5//! connection, the client, and [`TextInput`].
6
7use 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/// Indicates what triggered the change in selected text (including changes to
20/// the cursor location).
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum SelectionChangedCause {
23    /// The user tapped on the text and that caused the selection (or the location
24    /// of the cursor) to change.
25    Tap,
26
27    /// The user tapped twice in quick succession on the text and that caused
28    /// the selection (or the location of the cursor) to change.
29    DoubleTap,
30
31    /// The user long-pressed the text and that caused the selection (or the
32    /// location of the cursor) to change.
33    LongPress,
34
35    /// The user force-pressed the text and that caused the selection (or the
36    /// location of the cursor) to change.
37    ForcePress,
38
39    /// The user used the keyboard to change the selection or the location of the
40    /// cursor.
41    ///
42    /// Keyboard-triggered selection changes may be caused by the IME as well as
43    /// by accessibility tools (e.g. TalkBack on Android).
44    Keyboard,
45
46    /// The user used the selection toolbar to change the selection or the
47    /// location of the cursor.
48    ///
49    /// An example is when the user taps on select all in the tool bar.
50    Toolbar,
51
52    /// The user used the mouse to change the selection by dragging over a piece
53    /// of text.
54    Drag,
55
56    /// The user used stylus handwriting to change the selection.
57    ///
58    /// Currently, this is only supported on iPadOS 14+ via the Scribble feature,
59    /// or on Android API 34+ via the Scribe feature.
60    StylusHandwriting,
61}
62
63/// Represents a selection rect for a character and it's position in the text.
64///
65/// This is used to report the current text selection rect and position data
66/// to the engine for Scribble support on iPadOS 14.
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct SelectionRect {
69    /// The position of this selection rect within the text String.
70    pub position: i32,
71    /// The rectangle representing the bounds of this selection rect within the
72    /// currently focused `RenderEditable`'s coordinate space.
73    pub bounds: Rect,
74    /// The direction text flows within this selection rect.
75    pub direction: TextDirection,
76}
77
78impl SelectionRect {
79    /// Constructor for creating a [`SelectionRect`] from a text `position` and
80    /// `bounds`.
81    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/// Text styling information for the current input client.
97///
98/// See also:
99///
100///  * [`TextInputConnection::update_style`], which uses this class to send style
101///    information to the platform.
102#[derive(Clone, Debug, PartialEq)]
103pub struct TextInputStyle {
104    /// The name of the font to use when painting the text (e.g., Roboto).
105    pub font_family: Option<String>,
106    /// The size of fonts (in logical pixels) to use when painting the text.
107    pub font_size: Option<f64>,
108    /// The typeface thickness to use when painting the text (e.g., bold).
109    pub font_weight: Option<FontWeight>,
110    /// The directionality of the text.
111    pub text_direction: TextDirection,
112    /// How the text should be aligned horizontally.
113    pub text_align: TextAlign,
114    /// The amount of space (in logical pixels) to add between each letter.
115    pub letter_spacing: Option<f64>,
116    /// The amount of space (in logical pixels) to add at each sequence of
117    /// white-space (i.e. between each word).
118    pub word_spacing: Option<f64>,
119    /// The line height in logical pixels.
120    pub line_height: Option<f64>,
121}
122
123impl TextInputStyle {
124    /// Creates text styling information for the current input client.
125    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
139/// An interface to receive information from [`TextInput`].
140///
141/// See also:
142///
143///  * [`TextInput::attach`]
144///  * `EditableText`, a [`TextInputClient`] implementation.
145pub trait TextInputClient: Sized + 'static {
146    /// The current state of the [`TextEditingValue`] held by this client.
147    fn current_text_editing_value(self: Handle<Self>, app: &App) -> Option<TextEditingValue>;
148
149    /// Requests that this client update its editing state to the given value.
150    ///
151    /// The new `value` is treated as user input and thus may subject to input
152    /// formatting.
153    fn update_editing_value(self: Handle<Self>, app: &mut App, value: TextEditingValue);
154
155    /// Requests that this client perform the given action.
156    fn perform_action(self: Handle<Self>, app: &mut App, action: TextInputAction);
157
158    /// Request from the input method that this client perform the given private
159    /// command.
160    fn perform_private_command(self: Handle<Self>, app: &mut App, action: &str) {
161        let _ = (self, app, action);
162    }
163
164    /// Updates the floating cursor position and state.
165    fn update_floating_cursor(self: Handle<Self>, app: &mut App, point: RawFloatingCursorPoint) {
166        let _ = (self, app, point);
167    }
168
169    /// Requests that this client display a prompt rectangle for the given text range,
170    /// to indicate the range of text that will be changed by a pending autocorrection.
171    fn show_autocorrection_prompt_rect(self: Handle<Self>, app: &mut App, start: i32, end: i32) {
172        let _ = (self, app, start, end);
173    }
174
175    /// Platform notified framework of closed connection.
176    ///
177    /// [`TextInputClient`] should cleanup its connection and finalize editing.
178    fn connection_closed(self: Handle<Self>, app: &mut App);
179
180    /// Requests that the client show the editing toolbar, for example when the
181    /// platform changes the selection through a non-flutter method such as
182    /// scribble.
183    fn show_toolbar(self: Handle<Self>, app: &mut App) {
184        let _ = (self, app);
185    }
186
187    /// Requests that the client add a text placeholder to reserve visual space
188    /// in the text.
189    fn insert_text_placeholder(self: Handle<Self>, app: &mut App, size: Size) {
190        let _ = (self, app, size);
191    }
192
193    /// Requests that the client remove the text placeholder.
194    fn remove_text_placeholder(self: Handle<Self>, app: &mut App) {
195        let _ = (self, app);
196    }
197
198    /// Performs the specified MacOS-specific selector from the
199    /// `NSStandardKeyBindingResponding` protocol or user-specified selector
200    /// from `DefaultKeyBinding.Dict`.
201    fn perform_selector(self: Handle<Self>, app: &mut App, selector_name: &str) {
202        let _ = (self, app, selector_name);
203    }
204
205    /// This client as the erased [`AnyTextInputClient`].
206    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/// Erased [`TextInputClient`]: one identity and a static vtable.
261#[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
321/// An interface for manipulating the selection, to be used by the implementor
322/// of the toolbar widget.
323pub trait TextSelectionDelegate: Sized + 'static {
324    /// Gets the current text input.
325    fn text_editing_value(self: Handle<Self>, app: &App) -> TextEditingValue;
326
327    /// Indicates that the user has requested the delegate to replace its current
328    /// text editing state with `value`.
329    ///
330    /// The new `value` is treated as user input and thus may subject to input
331    /// formatting.
332    fn user_update_text_editing_value(
333        self: Handle<Self>,
334        app: &mut App,
335        value: TextEditingValue,
336        cause: SelectionChangedCause,
337    );
338
339    /// Hides the text selection toolbar.
340    ///
341    /// By default, `hide_handles` is true, and the toolbar is hidden along with its
342    /// handles. If `hide_handles` is set to false, then the toolbar will be hidden
343    /// but the handles will remain.
344    fn hide_toolbar(self: Handle<Self>, app: &mut App, hide_handles: bool);
345
346    /// Brings the provided [`TextPosition`] into the visible area of the text
347    /// input.
348    fn bring_into_view(self: Handle<Self>, app: &mut App, position: inset_embedder::TextPosition);
349
350    /// Whether cut is enabled.
351    fn cut_enabled(self: Handle<Self>, _app: &App) -> bool {
352        true
353    }
354
355    /// Whether copy is enabled.
356    fn copy_enabled(self: Handle<Self>, _app: &App) -> bool {
357        true
358    }
359
360    /// Whether paste is enabled.
361    fn paste_enabled(self: Handle<Self>, _app: &App) -> bool {
362        true
363    }
364
365    /// Whether select all is enabled.
366    fn select_all_enabled(self: Handle<Self>, _app: &App) -> bool {
367        true
368    }
369
370    /// Whether look up is enabled.
371    fn look_up_enabled(self: Handle<Self>, _app: &App) -> bool {
372        true
373    }
374
375    /// Whether search web is enabled.
376    fn search_web_enabled(self: Handle<Self>, _app: &App) -> bool {
377        true
378    }
379
380    /// Whether share is enabled.
381    fn share_enabled(self: Handle<Self>, _app: &App) -> bool {
382        true
383    }
384
385    /// Whether Live Text input is enabled.
386    fn live_text_input_enabled(self: Handle<Self>, _app: &App) -> bool {
387        false
388    }
389
390    /// Cut current selection to [`crate::Clipboard`].
391    fn cut_selection(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
392
393    /// Paste text from [`crate::Clipboard`].
394    fn paste_text(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause) -> Task<()>;
395
396    /// Set the current selection to contain the entire text value.
397    fn select_all(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
398
399    /// Copy current selection to [`crate::Clipboard`].
400    fn copy_selection(self: Handle<Self>, app: &mut App, cause: SelectionChangedCause);
401
402    /// This delegate as the erased [`AnyTextSelectionDelegate`].
403    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/// Erased [`TextSelectionDelegate`]: one identity and a static vtable.
456#[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
541/// An interface for interacting with a text input control.
542///
543/// See also:
544///
545///  * [`TextInput::attach`], a method used to establish a [`TextInputConnection`]
546///    between the system's text input and a [`TextInputClient`].
547pub 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    /// Whether this connection is currently interacting with the text input control.
560    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    /// Requests that the text input control become visible.
568    pub fn show(self: Handle<Self>, app: &mut App) {
569        debug_assert!(self.attached(app));
570        TextInput::instance(app).show(app);
571    }
572
573    /// Requests that the text input control update itself according to the new
574    /// [`TextInputConfiguration`].
575    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    /// Requests that the text input control change its internal state to match
581    /// the given state.
582    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    /// Send the size and transform of the editable text to engine.
588    ///
589    /// 1. `editable_box_size`: size of the render editable box.
590    ///
591    /// 2. `transform`: a matrix that maps the local paint coordinate system
592    ///    to the `PipelineOwner.rootNode`.
593    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    /// Send the smallest rect that covers the text in the client that's currently
613    /// being composed.
614    ///
615    /// If any of the 4 coordinates of the given [`Rect`] is not finite, a [`Rect`] of
616    /// size (-1, -1) will be sent instead.
617    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    /// Sends the coordinates of caret rect. This is used on macOS for positioning
631    /// the accent selection menu.
632    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    /// Send the bounding boxes of the current selected glyphs in the client to
646    /// the platform's text input plugin.
647    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    /// Send text styling information.
659    pub fn update_style(self: Handle<Self>, app: &mut App, style: TextInputStyle) {
660        debug_assert!(self.attached(app));
661        let _ = style;
662    }
663
664    /// Stop interacting with the text input control.
665    ///
666    /// After calling this method, the text input control might disappear if no
667    /// other client attaches to it within this animation frame.
668    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    /// Platform sent a notification informing the connection is closed.
676    ///
677    /// [`TextInputConnection`] should clean current client connection.
678    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
685/// An low-level interface to the system's text input control.
686///
687/// To start interacting with the system's text input control, call [`attach`](Self::attach) to
688/// establish a [`TextInputConnection`] between the system's text input control
689/// and a [`TextInputClient`]. The majority of commands available for
690/// interacting with the text input control reside in the returned
691/// [`TextInputConnection`].
692///
693/// Dart's `TextInput._instance` is [`instance`](Self::instance), the App's singleton.
694pub 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    /// Dart's `TextInput._instance`.
717    pub fn instance(app: &mut App) -> Handle<TextInput> {
718        app.singleton::<TextInput>()
719    }
720
721    /// The currently attached connection, if any.
722    pub fn current_connection(
723        self: Handle<Self>,
724        app: &App,
725    ) -> Option<Handle<TextInputConnection>> {
726        app.get(self).current_connection
727    }
728
729    /// Ensure that a [`TextInput`] instance has been set up so that the platform
730    /// can handle messages on the text input method channel.
731    pub fn ensure_initialized(app: &mut App) {
732        let _ = Self::instance(app);
733    }
734
735    /// Resets the internal ID counter for testing purposes.
736    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    /// Begin interacting with the text input control.
744    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    /// Requests the system autofill UI to appear.
770    ///
771    /// Currently only works on Android. Other platforms do not respond to this
772    /// message.
773    ///
774    /// See also:
775    ///
776    ///  * `EditableText`, a `TextInputClient` that calls this method when focused.
777    pub fn request_autofill(_app: &mut App) {}
778
779    /// Finishes the current autofill context, and potentially saves the user
780    /// input for future use if `should_save` is true.
781    ///
782    /// Typically, this method should be called when the user has finalized their
783    /// input. For example, in a `Form`, it's typically done immediately before or
784    /// after its content is submitted.
785    ///
786    /// The topmost [`crate::AutofillScope`]s also call [`finish_autofill_context`]
787    /// automatically when they are disposed.
788    ///
789    /// An autofill context is a collection of input fields that live in the
790    /// platform's text input plugin. The platform is encouraged to save the user
791    /// input stored in the current autofill context before the context is
792    /// destroyed, when [`TextInput::finish_autofill_context`] is called with
793    /// `should_save` set to true.
794    ///
795    /// Currently, there can only be at most one autofill context at any given
796    /// time. When any input field in an autofill group requests autofill (which
797    /// is done automatically when an autofillable `EditableText` gains focus),
798    /// the current autofill context will merge the content of that group into
799    /// itself. When there isn't an existing autofill context, one will be created
800    /// to hold the newly added input fields from the group.
801    ///
802    /// Once added to an autofill context, an input field will stay in the context
803    /// until the context is destroyed. To prevent leaks, call
804    /// [`TextInput::finish_autofill_context`] to signal the text input plugin that
805    /// the user has finalized their input in the current autofill context. The
806    /// platform text input plugin either encourages or discourages the platform
807    /// from saving the user input based on the value of the `should_save`
808    /// parameter. The platform usually shows a "Save for autofill?" prompt for
809    /// user confirmation.
810    ///
811    /// On many platforms, calling [`finish_autofill_context`] shows the save user
812    /// input dialog and disrupts the user's flow. Ideally the dialog should only
813    /// be shown no more than once for every screen. Consider removing premature
814    /// [`finish_autofill_context`] calls to prevent showing the save user input UI
815    /// too frequently. However, calling [`finish_autofill_context`] when there's no
816    /// existing autofill context usually does not bring up the save user input
817    /// UI.
818    ///
819    /// Dart's default is `shouldSave == true`.
820    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    /// Flutter `TextInputClient.updateEditingState`: the host has a new value.
912    ///
913    /// Does not echo the value back to the host (Flutter excludes
914    /// `_PlatformTextInputControl`).
915    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    /// Flutter `TextInputClient.performAction`.
924    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    /// Flutter `TextInputClient.onConnectionClosed`.
933    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
943/// The view the field is in, which names it in its configuration, as Flutter's engine finds
944/// the view by `viewId`.
945fn 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}