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, canvas, div,
292    };
293
294    #[gpui::test]
295    fn text_input_configuration_forwarded_only_on_change(cx: &mut TestAppContext) {
296        let custom = TextInputConfiguration {
297            autocorrect: true,
298            input_action: TextInputAction::Send,
299            ..Default::default()
300        };
301        let window = cx.add_window({
302            let custom = custom.clone();
303            move |_, cx| ConfigurationTestView {
304                focus_handle: cx.focus_handle(),
305                configuration: custom,
306            }
307        });
308        let view = window.root(cx).unwrap();
309        let test_window = cx.test_window(window.into());
310        let window = AnyWindowHandle::from(window);
311        let draw = |cx: &mut TestAppContext| {
312            cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx))
313                .unwrap();
314        };
315
316        // Nothing is focused, so the platform learns the default configuration.
317        draw(cx);
318        assert_eq!(
319            test_window.text_input_configurations(),
320            vec![TextInputConfiguration::default()]
321        );
322
323        // Focusing the view routes its configuration to the platform.
324        cx.update_window(window, |_, window, cx| {
325            let focus_handle = view.read(cx).focus_handle.clone();
326            window.focus(&focus_handle, cx);
327        })
328        .unwrap();
329        draw(cx);
330        assert_eq!(
331            test_window.text_input_configurations(),
332            vec![TextInputConfiguration::default(), custom.clone()]
333        );
334
335        // Redrawing without a change forwards nothing.
336        draw(cx);
337        assert_eq!(test_window.text_input_configurations().len(), 2);
338
339        // Changing the configuration forwards the new value.
340        let updated = TextInputConfiguration {
341            suggestions: true,
342            ..custom
343        };
344        view.update(cx, {
345            let updated = updated.clone();
346            |view, cx| {
347                view.configuration = updated;
348                cx.notify();
349            }
350        });
351        draw(cx);
352        assert_eq!(
353            test_window.text_input_configurations().last(),
354            Some(&updated)
355        );
356        assert_eq!(test_window.text_input_configurations().len(), 3);
357
358        // Losing focus reverts the platform to the default configuration.
359        cx.update_window(window, |_, window, cx| window.blur(cx))
360            .unwrap();
361        draw(cx);
362        assert_eq!(
363            test_window.text_input_configurations().last(),
364            Some(&TextInputConfiguration::default())
365        );
366        assert_eq!(test_window.text_input_configurations().len(), 4);
367    }
368
369    struct ConfigurationTestView {
370        focus_handle: FocusHandle,
371        configuration: TextInputConfiguration,
372    }
373
374    impl Render for ConfigurationTestView {
375        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
376            let view = cx.entity();
377            let focus_handle = self.focus_handle.clone();
378            div().size_full().track_focus(&self.focus_handle).child(
379                canvas(
380                    |_, _, _| {},
381                    move |bounds, _, window, cx| {
382                        window.handle_input(
383                            &focus_handle,
384                            ElementInputHandler::new(bounds, view),
385                            cx,
386                        );
387                    },
388                )
389                .size_full(),
390            )
391        }
392    }
393
394    impl EntityInputHandler for ConfigurationTestView {
395        fn text_for_range(
396            &mut self,
397            _range: std::ops::Range<usize>,
398            _adjusted_range: &mut Option<std::ops::Range<usize>>,
399            _window: &mut Window,
400            _cx: &mut Context<Self>,
401        ) -> Option<String> {
402            None
403        }
404
405        fn selected_text_range(
406            &mut self,
407            _ignore_disabled_input: bool,
408            _window: &mut Window,
409            _cx: &mut Context<Self>,
410        ) -> Option<UTF16Selection> {
411            None
412        }
413
414        fn marked_text_range(
415            &self,
416            _window: &mut Window,
417            _cx: &mut Context<Self>,
418        ) -> Option<std::ops::Range<usize>> {
419            None
420        }
421
422        fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
423
424        fn replace_text_in_range(
425            &mut self,
426            _range: Option<std::ops::Range<usize>>,
427            _text: &str,
428            _window: &mut Window,
429            _cx: &mut Context<Self>,
430        ) {
431        }
432
433        fn replace_and_mark_text_in_range(
434            &mut self,
435            _range: Option<std::ops::Range<usize>>,
436            _new_text: &str,
437            _new_selected_range: Option<std::ops::Range<usize>>,
438            _window: &mut Window,
439            _cx: &mut Context<Self>,
440        ) {
441        }
442
443        fn bounds_for_range(
444            &mut self,
445            _range_utf16: std::ops::Range<usize>,
446            _element_bounds: Bounds<Pixels>,
447            _window: &mut Window,
448            _cx: &mut Context<Self>,
449        ) -> Option<Bounds<Pixels>> {
450            None
451        }
452
453        fn character_index_for_point(
454            &mut self,
455            _point: crate::Point<Pixels>,
456            _window: &mut Window,
457            _cx: &mut Context<Self>,
458        ) -> Option<usize> {
459            None
460        }
461
462        fn text_input_configuration(
463            &mut self,
464            _window: &mut Window,
465            _cx: &mut Context<Self>,
466        ) -> TextInputConfiguration {
467            self.configuration.clone()
468        }
469    }
470}