Skip to main content

gpui/
input.rs

1use crate::{
2    App, Bounds, ClipboardItem, Context, Entity, InputHandler, Pixels, TextInputConfiguration,
3    UTF16Selection, Window,
4};
5use std::ops::Range;
6
7/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
8///
9/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler<V>`].
10/// This input handler can then be assigned during paint by calling [`Window::handle_input`].
11///
12/// See [`InputHandler`] for details on how to implement each method.
13pub trait EntityInputHandler: 'static + Sized {
14    /// See [`InputHandler::text_for_range`] for details
15    fn text_for_range(
16        &mut self,
17        range: Range<usize>,
18        adjusted_range: &mut Option<Range<usize>>,
19        window: &mut Window,
20        cx: &mut Context<Self>,
21    ) -> Option<String>;
22
23    /// See [`InputHandler::selected_text_range`] for details
24    fn selected_text_range(
25        &mut self,
26        ignore_disabled_input: bool,
27        window: &mut Window,
28        cx: &mut Context<Self>,
29    ) -> Option<UTF16Selection>;
30
31    /// See [`InputHandler::marked_text_range`] for details
32    fn marked_text_range(
33        &self,
34        window: &mut Window,
35        cx: &mut Context<Self>,
36    ) -> Option<Range<usize>>;
37
38    /// See [`InputHandler::unmark_text`] for details
39    fn unmark_text(&mut self, window: &mut Window, cx: &mut Context<Self>);
40
41    /// See [`InputHandler::paste`] for details
42    fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut Context<Self>) {
43        if let Some(text) = item.text() {
44            self.replace_text_in_range(None, &text, window, cx);
45        }
46    }
47
48    /// See [`InputHandler::replace_text_in_range`] for details
49    fn replace_text_in_range(
50        &mut self,
51        range: Option<Range<usize>>,
52        text: &str,
53        window: &mut Window,
54        cx: &mut Context<Self>,
55    );
56
57    /// See [`InputHandler::replace_and_mark_text_in_range`] for details
58    fn replace_and_mark_text_in_range(
59        &mut self,
60        range: Option<Range<usize>>,
61        new_text: &str,
62        new_selected_range: Option<Range<usize>>,
63        window: &mut Window,
64        cx: &mut Context<Self>,
65    );
66
67    /// See [`InputHandler::bounds_for_range`] for details
68    fn bounds_for_range(
69        &mut self,
70        range_utf16: Range<usize>,
71        element_bounds: Bounds<Pixels>,
72        window: &mut Window,
73        cx: &mut Context<Self>,
74    ) -> Option<Bounds<Pixels>>;
75
76    /// See [`InputHandler::character_index_for_point`] for details
77    fn character_index_for_point(
78        &mut self,
79        point: crate::Point<Pixels>,
80        window: &mut Window,
81        cx: &mut Context<Self>,
82    ) -> Option<usize>;
83
84    /// See [`InputHandler::set_selected_text_range`] for details
85    fn set_selected_text_range(
86        &mut self,
87        _range_utf16: Range<usize>,
88        _window: &mut Window,
89        _cx: &mut Context<Self>,
90    ) {
91    }
92
93    /// See [`InputHandler::text_length_utf16`] for details
94    fn text_length_utf16(
95        &mut self,
96        _window: &mut Window,
97        _cx: &mut Context<Self>,
98    ) -> Option<usize> {
99        None
100    }
101
102    /// See [`InputHandler::accepts_text_input`] for details
103    fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
104        true
105    }
106
107    /// See [`InputHandler::text_input_configuration`] for details
108    fn text_input_configuration(
109        &mut self,
110        _window: &mut Window,
111        _cx: &mut Context<Self>,
112    ) -> TextInputConfiguration {
113        TextInputConfiguration::default()
114    }
115
116    /// See [`InputHandler::text_input_editable_range`] for details
117    fn text_input_editable_range(
118        &mut self,
119        _window: &mut Window,
120        _cx: &mut Context<Self>,
121    ) -> Option<Range<usize>> {
122        None
123    }
124}
125
126/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`]
127/// with an instance during your element's paint.
128pub struct ElementInputHandler<V> {
129    view: Entity<V>,
130    element_bounds: Bounds<Pixels>,
131}
132
133impl<V: 'static> ElementInputHandler<V> {
134    /// Used in [`Element::paint`][element_paint] with the element's bounds, a `Window`, and a `App` context.
135    ///
136    /// [element_paint]: crate::Element::paint
137    pub fn new(element_bounds: Bounds<Pixels>, view: Entity<V>) -> Self {
138        ElementInputHandler {
139            view,
140            element_bounds,
141        }
142    }
143}
144
145impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
146    fn selected_text_range(
147        &mut self,
148        ignore_disabled_input: bool,
149        window: &mut Window,
150        cx: &mut App,
151    ) -> Option<UTF16Selection> {
152        self.view.update(cx, |view, cx| {
153            view.selected_text_range(ignore_disabled_input, window, cx)
154        })
155    }
156
157    fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>> {
158        self.view
159            .update(cx, |view, cx| view.marked_text_range(window, cx))
160    }
161
162    fn text_for_range(
163        &mut self,
164        range_utf16: Range<usize>,
165        adjusted_range: &mut Option<Range<usize>>,
166        window: &mut Window,
167        cx: &mut App,
168    ) -> Option<String> {
169        self.view.update(cx, |view, cx| {
170            view.text_for_range(range_utf16, adjusted_range, window, cx)
171        })
172    }
173
174    fn replace_text_in_range(
175        &mut self,
176        replacement_range: Option<Range<usize>>,
177        text: &str,
178        window: &mut Window,
179        cx: &mut App,
180    ) {
181        self.view.update(cx, |view, cx| {
182            view.replace_text_in_range(replacement_range, text, window, cx)
183        });
184    }
185
186    fn replace_and_mark_text_in_range(
187        &mut self,
188        range_utf16: Option<Range<usize>>,
189        new_text: &str,
190        new_selected_range: Option<Range<usize>>,
191        window: &mut Window,
192        cx: &mut App,
193    ) {
194        self.view.update(cx, |view, cx| {
195            view.replace_and_mark_text_in_range(
196                range_utf16,
197                new_text,
198                new_selected_range,
199                window,
200                cx,
201            )
202        });
203    }
204
205    fn unmark_text(&mut self, window: &mut Window, cx: &mut App) {
206        self.view
207            .update(cx, |view, cx| view.unmark_text(window, cx));
208    }
209
210    fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
211        self.view
212            .update(cx, |view, cx| view.paste(item, window, cx));
213    }
214
215    fn bounds_for_range(
216        &mut self,
217        range_utf16: Range<usize>,
218        window: &mut Window,
219        cx: &mut App,
220    ) -> Option<Bounds<Pixels>> {
221        self.view.update(cx, |view, cx| {
222            view.bounds_for_range(range_utf16, self.element_bounds, window, cx)
223        })
224    }
225
226    fn character_index_for_point(
227        &mut self,
228        point: crate::Point<Pixels>,
229        window: &mut Window,
230        cx: &mut App,
231    ) -> Option<usize> {
232        self.view.update(cx, |view, cx| {
233            view.character_index_for_point(point, window, cx)
234        })
235    }
236
237    fn set_selected_text_range(
238        &mut self,
239        range_utf16: Range<usize>,
240        window: &mut Window,
241        cx: &mut App,
242    ) {
243        self.view.update(cx, |view, cx| {
244            view.set_selected_text_range(range_utf16, window, cx)
245        })
246    }
247
248    fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
249        Some(self.element_bounds)
250    }
251
252    fn text_length_utf16(&mut self, window: &mut Window, cx: &mut App) -> Option<usize> {
253        self.view
254            .update(cx, |view, cx| view.text_length_utf16(window, cx))
255    }
256
257    fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
258        self.view
259            .update(cx, |view, cx| view.accepts_text_input(window, cx))
260    }
261
262    fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool {
263        self.view
264            .update(cx, |view, cx| view.accepts_text_input(window, cx))
265    }
266
267    fn text_input_configuration(
268        &mut self,
269        window: &mut Window,
270        cx: &mut App,
271    ) -> TextInputConfiguration {
272        self.view
273            .update(cx, |view, cx| view.text_input_configuration(window, cx))
274    }
275
276    fn text_input_editable_range(
277        &mut self,
278        window: &mut Window,
279        cx: &mut App,
280    ) -> Option<Range<usize>> {
281        self.view
282            .update(cx, |view, cx| view.text_input_editable_range(window, cx))
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::{
290        AnyWindowHandle, AppContext as _, FocusHandle, InteractiveElement as _, IntoElement,
291        ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction,
292        TextInputStateChange, canvas, div,
293    };
294
295    #[gpui::test]
296    fn text_input_configuration_and_focus_state_are_forwarded_on_change(cx: &mut TestAppContext) {
297        let custom = TextInputConfiguration {
298            autocorrect: true,
299            input_action: TextInputAction::Send,
300            ..Default::default()
301        };
302        let window = cx.add_window({
303            let custom = custom.clone();
304            move |_, cx| ConfigurationTestView {
305                focus_handle: cx.focus_handle(),
306                configuration: custom,
307            }
308        });
309        let view = window.root(cx).unwrap();
310        let test_window = cx.test_window(window.into());
311        let window = AnyWindowHandle::from(window);
312        let draw = |cx: &mut TestAppContext| {
313            cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx))
314                .unwrap();
315        };
316
317        // Nothing is focused, so the platform learns the default configuration.
318        draw(cx);
319        assert_eq!(
320            test_window.text_input_configurations(),
321            vec![TextInputConfiguration::default()]
322        );
323        assert!(test_window.text_input_state_changes().is_empty());
324
325        // Focusing the view routes its configuration to the platform.
326        cx.update_window(window, |_, window, cx| {
327            let focus_handle = view.read(cx).focus_handle.clone();
328            window.focus(&focus_handle, cx);
329        })
330        .unwrap();
331        draw(cx);
332        assert_eq!(
333            test_window.text_input_configurations(),
334            vec![TextInputConfiguration::default(), custom.clone()]
335        );
336        assert_eq!(
337            test_window.text_input_state_changes(),
338            vec![TextInputStateChange::FocusGained]
339        );
340
341        // Redrawing without a change forwards nothing.
342        draw(cx);
343        assert_eq!(test_window.text_input_configurations().len(), 2);
344        assert_eq!(test_window.text_input_state_changes().len(), 1);
345
346        // Changing the configuration forwards the new value.
347        let updated = TextInputConfiguration {
348            suggestions: true,
349            ..custom
350        };
351        view.update(cx, {
352            let updated = updated.clone();
353            |view, cx| {
354                view.configuration = updated;
355                cx.notify();
356            }
357        });
358        draw(cx);
359        assert_eq!(
360            test_window.text_input_configurations().last(),
361            Some(&updated)
362        );
363        assert_eq!(test_window.text_input_configurations().len(), 3);
364        assert_eq!(test_window.text_input_state_changes().len(), 1);
365
366        // Losing focus reverts the platform to the default configuration.
367        cx.update_window(window, |_, window, cx| window.blur(cx))
368            .unwrap();
369        draw(cx);
370        assert_eq!(
371            test_window.text_input_configurations().last(),
372            Some(&TextInputConfiguration::default())
373        );
374        assert_eq!(test_window.text_input_configurations().len(), 4);
375        assert_eq!(
376            test_window.text_input_state_changes(),
377            vec![
378                TextInputStateChange::FocusGained,
379                TextInputStateChange::FocusLost
380            ]
381        );
382    }
383
384    struct ConfigurationTestView {
385        focus_handle: FocusHandle,
386        configuration: TextInputConfiguration,
387    }
388
389    impl Render for ConfigurationTestView {
390        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
391            let view = cx.entity();
392            let focus_handle = self.focus_handle.clone();
393            div().size_full().track_focus(&self.focus_handle).child(
394                canvas(
395                    |_, _, _| {},
396                    move |bounds, _, window, cx| {
397                        window.handle_input(
398                            &focus_handle,
399                            ElementInputHandler::new(bounds, view),
400                            cx,
401                        );
402                    },
403                )
404                .size_full(),
405            )
406        }
407    }
408
409    impl EntityInputHandler for ConfigurationTestView {
410        fn text_for_range(
411            &mut self,
412            _range: std::ops::Range<usize>,
413            _adjusted_range: &mut Option<std::ops::Range<usize>>,
414            _window: &mut Window,
415            _cx: &mut Context<Self>,
416        ) -> Option<String> {
417            None
418        }
419
420        fn selected_text_range(
421            &mut self,
422            _ignore_disabled_input: bool,
423            _window: &mut Window,
424            _cx: &mut Context<Self>,
425        ) -> Option<UTF16Selection> {
426            None
427        }
428
429        fn marked_text_range(
430            &self,
431            _window: &mut Window,
432            _cx: &mut Context<Self>,
433        ) -> Option<std::ops::Range<usize>> {
434            None
435        }
436
437        fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
438
439        fn replace_text_in_range(
440            &mut self,
441            _range: Option<std::ops::Range<usize>>,
442            _text: &str,
443            _window: &mut Window,
444            _cx: &mut Context<Self>,
445        ) {
446        }
447
448        fn replace_and_mark_text_in_range(
449            &mut self,
450            _range: Option<std::ops::Range<usize>>,
451            _new_text: &str,
452            _new_selected_range: Option<std::ops::Range<usize>>,
453            _window: &mut Window,
454            _cx: &mut Context<Self>,
455        ) {
456        }
457
458        fn bounds_for_range(
459            &mut self,
460            _range_utf16: std::ops::Range<usize>,
461            _element_bounds: Bounds<Pixels>,
462            _window: &mut Window,
463            _cx: &mut Context<Self>,
464        ) -> Option<Bounds<Pixels>> {
465            None
466        }
467
468        fn character_index_for_point(
469            &mut self,
470            _point: crate::Point<Pixels>,
471            _window: &mut Window,
472            _cx: &mut Context<Self>,
473        ) -> Option<usize> {
474            None
475        }
476
477        fn text_input_configuration(
478            &mut self,
479            _window: &mut Window,
480            _cx: &mut Context<Self>,
481        ) -> TextInputConfiguration {
482            self.configuration.clone()
483        }
484    }
485}