input.rs

  1use std::ops::Range;
  2
  3use gpui::{
  4    App, Application, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler,
  5    Entity, EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, Keystroke,
  6    LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
  7    ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, WindowBounds,
  8    WindowOptions, actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative,
  9    rgb, rgba, size, white, yellow,
 10};
 11use unicode_segmentation::*;
 12
 13actions!(
 14    text_input,
 15    [
 16        Backspace,
 17        Delete,
 18        Left,
 19        Right,
 20        SelectLeft,
 21        SelectRight,
 22        SelectAll,
 23        Home,
 24        End,
 25        ShowCharacterPalette,
 26        Paste,
 27        Cut,
 28        Copy,
 29    ]
 30);
 31
 32struct TextInput {
 33    focus_handle: FocusHandle,
 34    content: SharedString,
 35    placeholder: SharedString,
 36    selected_range: Range<usize>,
 37    selection_reversed: bool,
 38    marked_range: Option<Range<usize>>,
 39    last_layout: Option<ShapedLine>,
 40    last_bounds: Option<Bounds<Pixels>>,
 41    is_selecting: bool,
 42}
 43
 44impl TextInput {
 45    fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
 46        if self.selected_range.is_empty() {
 47            self.move_to(self.previous_boundary(self.cursor_offset()), cx);
 48        } else {
 49            self.move_to(self.selected_range.start, cx)
 50        }
 51    }
 52
 53    fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
 54        if self.selected_range.is_empty() {
 55            self.move_to(self.next_boundary(self.selected_range.end), cx);
 56        } else {
 57            self.move_to(self.selected_range.end, cx)
 58        }
 59    }
 60
 61    fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
 62        self.select_to(self.previous_boundary(self.cursor_offset()), cx);
 63    }
 64
 65    fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
 66        self.select_to(self.next_boundary(self.cursor_offset()), cx);
 67    }
 68
 69    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
 70        self.move_to(0, cx);
 71        self.select_to(self.content.len(), cx)
 72    }
 73
 74    fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
 75        self.move_to(0, cx);
 76    }
 77
 78    fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
 79        self.move_to(self.content.len(), cx);
 80    }
 81
 82    fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
 83        if self.selected_range.is_empty() {
 84            self.select_to(self.previous_boundary(self.cursor_offset()), cx)
 85        }
 86        self.replace_text_in_range(None, "", window, cx)
 87    }
 88
 89    fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
 90        if self.selected_range.is_empty() {
 91            self.select_to(self.next_boundary(self.cursor_offset()), cx)
 92        }
 93        self.replace_text_in_range(None, "", window, cx)
 94    }
 95
 96    fn on_mouse_down(
 97        &mut self,
 98        event: &MouseDownEvent,
 99        _window: &mut Window,
100        cx: &mut Context<Self>,
101    ) {
102        self.is_selecting = true;
103
104        if event.modifiers.shift {
105            self.select_to(self.index_for_mouse_position(event.position), cx);
106        } else {
107            self.move_to(self.index_for_mouse_position(event.position), cx)
108        }
109    }
110
111    fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
112        self.is_selecting = false;
113    }
114
115    fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
116        if self.is_selecting {
117            self.select_to(self.index_for_mouse_position(event.position), cx);
118        }
119    }
120
121    fn show_character_palette(
122        &mut self,
123        _: &ShowCharacterPalette,
124        window: &mut Window,
125        _: &mut Context<Self>,
126    ) {
127        window.show_character_palette();
128    }
129
130    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
131        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
132            self.replace_text_in_range(None, &text.replace("\n", " "), window, cx);
133        }
134    }
135
136    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
137        if !self.selected_range.is_empty() {
138            cx.write_to_clipboard(ClipboardItem::new_string(
139                (&self.content[self.selected_range.clone()]).to_string(),
140            ));
141        }
142    }
143    fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
144        if !self.selected_range.is_empty() {
145            cx.write_to_clipboard(ClipboardItem::new_string(
146                (&self.content[self.selected_range.clone()]).to_string(),
147            ));
148            self.replace_text_in_range(None, "", window, cx)
149        }
150    }
151
152    fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
153        self.selected_range = offset..offset;
154        cx.notify()
155    }
156
157    fn cursor_offset(&self) -> usize {
158        if self.selection_reversed {
159            self.selected_range.start
160        } else {
161            self.selected_range.end
162        }
163    }
164
165    fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
166        if self.content.is_empty() {
167            return 0;
168        }
169
170        let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
171        else {
172            return 0;
173        };
174        if position.y < bounds.top() {
175            return 0;
176        }
177        if position.y > bounds.bottom() {
178            return self.content.len();
179        }
180        line.closest_index_for_x(position.x - bounds.left())
181    }
182
183    fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
184        if self.selection_reversed {
185            self.selected_range.start = offset
186        } else {
187            self.selected_range.end = offset
188        };
189        if self.selected_range.end < self.selected_range.start {
190            self.selection_reversed = !self.selection_reversed;
191            self.selected_range = self.selected_range.end..self.selected_range.start;
192        }
193        cx.notify()
194    }
195
196    fn offset_from_utf16(&self, offset: usize) -> usize {
197        let mut utf8_offset = 0;
198        let mut utf16_count = 0;
199
200        for ch in self.content.chars() {
201            if utf16_count >= offset {
202                break;
203            }
204            utf16_count += ch.len_utf16();
205            utf8_offset += ch.len_utf8();
206        }
207
208        utf8_offset
209    }
210
211    fn offset_to_utf16(&self, offset: usize) -> usize {
212        let mut utf16_offset = 0;
213        let mut utf8_count = 0;
214
215        for ch in self.content.chars() {
216            if utf8_count >= offset {
217                break;
218            }
219            utf8_count += ch.len_utf8();
220            utf16_offset += ch.len_utf16();
221        }
222
223        utf16_offset
224    }
225
226    fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
227        self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
228    }
229
230    fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
231        self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
232    }
233
234    fn previous_boundary(&self, offset: usize) -> usize {
235        self.content
236            .grapheme_indices(true)
237            .rev()
238            .find_map(|(idx, _)| (idx < offset).then_some(idx))
239            .unwrap_or(0)
240    }
241
242    fn next_boundary(&self, offset: usize) -> usize {
243        self.content
244            .grapheme_indices(true)
245            .find_map(|(idx, _)| (idx > offset).then_some(idx))
246            .unwrap_or(self.content.len())
247    }
248
249    fn reset(&mut self) {
250        self.content = "".into();
251        self.selected_range = 0..0;
252        self.selection_reversed = false;
253        self.marked_range = None;
254        self.last_layout = None;
255        self.last_bounds = None;
256        self.is_selecting = false;
257    }
258}
259
260impl EntityInputHandler for TextInput {
261    fn text_for_range(
262        &mut self,
263        range_utf16: Range<usize>,
264        actual_range: &mut Option<Range<usize>>,
265        _window: &mut Window,
266        _cx: &mut Context<Self>,
267    ) -> Option<String> {
268        let range = self.range_from_utf16(&range_utf16);
269        actual_range.replace(self.range_to_utf16(&range));
270        Some(self.content[range].to_string())
271    }
272
273    fn selected_text_range(
274        &mut self,
275        _ignore_disabled_input: bool,
276        _window: &mut Window,
277        _cx: &mut Context<Self>,
278    ) -> Option<UTF16Selection> {
279        Some(UTF16Selection {
280            range: self.range_to_utf16(&self.selected_range),
281            reversed: self.selection_reversed,
282        })
283    }
284
285    fn marked_text_range(
286        &self,
287        _window: &mut Window,
288        _cx: &mut Context<Self>,
289    ) -> Option<Range<usize>> {
290        self.marked_range
291            .as_ref()
292            .map(|range| self.range_to_utf16(range))
293    }
294
295    fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
296        self.marked_range = None;
297    }
298
299    fn replace_text_in_range(
300        &mut self,
301        range_utf16: Option<Range<usize>>,
302        new_text: &str,
303        _: &mut Window,
304        cx: &mut Context<Self>,
305    ) {
306        let range = range_utf16
307            .as_ref()
308            .map(|range_utf16| self.range_from_utf16(range_utf16))
309            .or(self.marked_range.clone())
310            .unwrap_or(self.selected_range.clone());
311
312        self.content =
313            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
314                .into();
315        self.selected_range = range.start + new_text.len()..range.start + new_text.len();
316        self.marked_range.take();
317        cx.notify();
318    }
319
320    fn replace_and_mark_text_in_range(
321        &mut self,
322        range_utf16: Option<Range<usize>>,
323        new_text: &str,
324        new_selected_range_utf16: Option<Range<usize>>,
325        _window: &mut Window,
326        cx: &mut Context<Self>,
327    ) {
328        let range = range_utf16
329            .as_ref()
330            .map(|range_utf16| self.range_from_utf16(range_utf16))
331            .or(self.marked_range.clone())
332            .unwrap_or(self.selected_range.clone());
333
334        self.content =
335            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
336                .into();
337        if !new_text.is_empty() {
338            self.marked_range = Some(range.start..range.start + new_text.len());
339        } else {
340            self.marked_range = None;
341        }
342        self.selected_range = new_selected_range_utf16
343            .as_ref()
344            .map(|range_utf16| self.range_from_utf16(range_utf16))
345            .map(|new_range| new_range.start + range.start..new_range.end + range.end)
346            .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
347
348        cx.notify();
349    }
350
351    fn bounds_for_range(
352        &mut self,
353        range_utf16: Range<usize>,
354        bounds: Bounds<Pixels>,
355        _window: &mut Window,
356        _cx: &mut Context<Self>,
357    ) -> Option<Bounds<Pixels>> {
358        let last_layout = self.last_layout.as_ref()?;
359        let range = self.range_from_utf16(&range_utf16);
360        Some(Bounds::from_corners(
361            point(
362                bounds.left() + last_layout.x_for_index(range.start),
363                bounds.top(),
364            ),
365            point(
366                bounds.left() + last_layout.x_for_index(range.end),
367                bounds.bottom(),
368            ),
369        ))
370    }
371
372    fn character_index_for_point(
373        &mut self,
374        point: gpui::Point<Pixels>,
375        _window: &mut Window,
376        _cx: &mut Context<Self>,
377    ) -> Option<usize> {
378        let line_point = self.last_bounds?.localize(&point)?;
379        let last_layout = self.last_layout.as_ref()?;
380
381        assert_eq!(last_layout.text, self.content);
382        let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
383        Some(self.offset_to_utf16(utf8_index))
384    }
385}
386
387struct TextElement {
388    input: Entity<TextInput>,
389}
390
391struct PrepaintState {
392    line: Option<ShapedLine>,
393    cursor: Option<PaintQuad>,
394    selection: Option<PaintQuad>,
395}
396
397impl IntoElement for TextElement {
398    type Element = Self;
399
400    fn into_element(self) -> Self::Element {
401        self
402    }
403}
404
405impl Element for TextElement {
406    type RequestLayoutState = ();
407    type PrepaintState = PrepaintState;
408
409    fn id(&self) -> Option<ElementId> {
410        None
411    }
412
413    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
414        None
415    }
416
417    fn request_layout(
418        &mut self,
419        _id: Option<&GlobalElementId>,
420        _inspector_id: Option<&gpui::InspectorElementId>,
421        window: &mut Window,
422        cx: &mut App,
423    ) -> (LayoutId, Self::RequestLayoutState) {
424        let mut style = Style::default();
425        style.size.width = relative(1.).into();
426        style.size.height = window.line_height().into();
427        (window.request_layout(style, [], cx), ())
428    }
429
430    fn prepaint(
431        &mut self,
432        _id: Option<&GlobalElementId>,
433        _inspector_id: Option<&gpui::InspectorElementId>,
434        bounds: Bounds<Pixels>,
435        _request_layout: &mut Self::RequestLayoutState,
436        window: &mut Window,
437        cx: &mut App,
438    ) -> Self::PrepaintState {
439        let input = self.input.read(cx);
440        let content = input.content.clone();
441        let selected_range = input.selected_range.clone();
442        let cursor = input.cursor_offset();
443        let style = window.text_style();
444
445        let (display_text, text_color) = if content.is_empty() {
446            (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
447        } else {
448            (content.clone(), style.color)
449        };
450
451        let run = TextRun {
452            len: display_text.len(),
453            font: style.font(),
454            color: text_color,
455            background_color: None,
456            underline: None,
457            strikethrough: None,
458        };
459        let runs = if let Some(marked_range) = input.marked_range.as_ref() {
460            vec![
461                TextRun {
462                    len: marked_range.start,
463                    ..run.clone()
464                },
465                TextRun {
466                    len: marked_range.end - marked_range.start,
467                    underline: Some(UnderlineStyle {
468                        color: Some(run.color),
469                        thickness: px(1.0),
470                        wavy: false,
471                    }),
472                    ..run.clone()
473                },
474                TextRun {
475                    len: display_text.len() - marked_range.end,
476                    ..run.clone()
477                },
478            ]
479            .into_iter()
480            .filter(|run| run.len > 0)
481            .collect()
482        } else {
483            vec![run]
484        };
485
486        let font_size = style.font_size.to_pixels(window.rem_size());
487        let line = window
488            .text_system()
489            .shape_line(display_text, font_size, &runs);
490
491        let cursor_pos = line.x_for_index(cursor);
492        let (selection, cursor) = if selected_range.is_empty() {
493            (
494                None,
495                Some(fill(
496                    Bounds::new(
497                        point(bounds.left() + cursor_pos, bounds.top()),
498                        size(px(2.), bounds.bottom() - bounds.top()),
499                    ),
500                    gpui::blue(),
501                )),
502            )
503        } else {
504            (
505                Some(fill(
506                    Bounds::from_corners(
507                        point(
508                            bounds.left() + line.x_for_index(selected_range.start),
509                            bounds.top(),
510                        ),
511                        point(
512                            bounds.left() + line.x_for_index(selected_range.end),
513                            bounds.bottom(),
514                        ),
515                    ),
516                    rgba(0x3311ff30),
517                )),
518                None,
519            )
520        };
521        PrepaintState {
522            line: Some(line),
523            cursor,
524            selection,
525        }
526    }
527
528    fn paint(
529        &mut self,
530        _id: Option<&GlobalElementId>,
531        _inspector_id: Option<&gpui::InspectorElementId>,
532        bounds: Bounds<Pixels>,
533        _request_layout: &mut Self::RequestLayoutState,
534        prepaint: &mut Self::PrepaintState,
535        window: &mut Window,
536        cx: &mut App,
537    ) {
538        let focus_handle = self.input.read(cx).focus_handle.clone();
539        window.handle_input(
540            &focus_handle,
541            ElementInputHandler::new(bounds, self.input.clone()),
542            cx,
543        );
544        if let Some(selection) = prepaint.selection.take() {
545            window.paint_quad(selection)
546        }
547        let line = prepaint.line.take().unwrap();
548        line.paint(bounds.origin, window.line_height(), window, cx)
549            .unwrap();
550
551        if focus_handle.is_focused(window) {
552            if let Some(cursor) = prepaint.cursor.take() {
553                window.paint_quad(cursor);
554            }
555        }
556
557        self.input.update(cx, |input, _cx| {
558            input.last_layout = Some(line);
559            input.last_bounds = Some(bounds);
560        });
561    }
562}
563
564impl Render for TextInput {
565    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
566        div()
567            .flex()
568            .key_context("TextInput")
569            .track_focus(&self.focus_handle(cx))
570            .cursor(CursorStyle::IBeam)
571            .on_action(cx.listener(Self::backspace))
572            .on_action(cx.listener(Self::delete))
573            .on_action(cx.listener(Self::left))
574            .on_action(cx.listener(Self::right))
575            .on_action(cx.listener(Self::select_left))
576            .on_action(cx.listener(Self::select_right))
577            .on_action(cx.listener(Self::select_all))
578            .on_action(cx.listener(Self::home))
579            .on_action(cx.listener(Self::end))
580            .on_action(cx.listener(Self::show_character_palette))
581            .on_action(cx.listener(Self::paste))
582            .on_action(cx.listener(Self::cut))
583            .on_action(cx.listener(Self::copy))
584            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
585            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
586            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
587            .on_mouse_move(cx.listener(Self::on_mouse_move))
588            .bg(rgb(0xeeeeee))
589            .line_height(px(30.))
590            .text_size(px(24.))
591            .child(
592                div()
593                    .h(px(30. + 4. * 2.))
594                    .w_full()
595                    .p(px(4.))
596                    .bg(white())
597                    .child(TextElement {
598                        input: cx.entity().clone(),
599                    }),
600            )
601    }
602}
603
604impl Focusable for TextInput {
605    fn focus_handle(&self, _: &App) -> FocusHandle {
606        self.focus_handle.clone()
607    }
608}
609
610struct InputExample {
611    text_input: Entity<TextInput>,
612    recent_keystrokes: Vec<Keystroke>,
613    focus_handle: FocusHandle,
614}
615
616impl Focusable for InputExample {
617    fn focus_handle(&self, _: &App) -> FocusHandle {
618        self.focus_handle.clone()
619    }
620}
621
622impl InputExample {
623    fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
624        self.recent_keystrokes.clear();
625        self.text_input
626            .update(cx, |text_input, _cx| text_input.reset());
627        cx.notify();
628    }
629}
630
631impl Render for InputExample {
632    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
633        div()
634            .bg(rgb(0xaaaaaa))
635            .track_focus(&self.focus_handle(cx))
636            .flex()
637            .flex_col()
638            .size_full()
639            .child(
640                div()
641                    .bg(white())
642                    .border_b_1()
643                    .border_color(black())
644                    .flex()
645                    .flex_row()
646                    .justify_between()
647                    .child(format!("Keyboard {}", cx.keyboard_layout().name()))
648                    .child(
649                        div()
650                            .border_1()
651                            .border_color(black())
652                            .px_2()
653                            .bg(yellow())
654                            .child("Reset")
655                            .hover(|style| {
656                                style
657                                    .bg(yellow().blend(opaque_grey(0.5, 0.5)))
658                                    .cursor_pointer()
659                            })
660                            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
661                    ),
662            )
663            .child(self.text_input.clone())
664            .children(self.recent_keystrokes.iter().rev().map(|ks| {
665                format!(
666                    "{:} {}",
667                    ks.unparse(),
668                    if let Some(key_char) = ks.key_char.as_ref() {
669                        format!("-> {:?}", key_char)
670                    } else {
671                        "".to_owned()
672                    }
673                )
674            }))
675    }
676}
677
678fn main() {
679    Application::new().run(|cx: &mut App| {
680        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
681        cx.bind_keys([
682            KeyBinding::new("backspace", Backspace, None),
683            KeyBinding::new("delete", Delete, None),
684            KeyBinding::new("left", Left, None),
685            KeyBinding::new("right", Right, None),
686            KeyBinding::new("shift-left", SelectLeft, None),
687            KeyBinding::new("shift-right", SelectRight, None),
688            KeyBinding::new("cmd-a", SelectAll, None),
689            KeyBinding::new("cmd-v", Paste, None),
690            KeyBinding::new("cmd-c", Copy, None),
691            KeyBinding::new("cmd-x", Cut, None),
692            KeyBinding::new("home", Home, None),
693            KeyBinding::new("end", End, None),
694            KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
695        ]);
696
697        let window = cx
698            .open_window(
699                WindowOptions {
700                    window_bounds: Some(WindowBounds::Windowed(bounds)),
701                    ..Default::default()
702                },
703                |_, cx| {
704                    let text_input = cx.new(|cx| TextInput {
705                        focus_handle: cx.focus_handle(),
706                        content: "".into(),
707                        placeholder: "Type here...".into(),
708                        selected_range: 0..0,
709                        selection_reversed: false,
710                        marked_range: None,
711                        last_layout: None,
712                        last_bounds: None,
713                        is_selecting: false,
714                    });
715                    cx.new(|cx| InputExample {
716                        text_input,
717                        recent_keystrokes: vec![],
718                        focus_handle: cx.focus_handle(),
719                    })
720                },
721            )
722            .unwrap();
723        let view = window.update(cx, |_, _, cx| cx.entity()).unwrap();
724        cx.observe_keystrokes(move |ev, _, cx| {
725            view.update(cx, |view, cx| {
726                view.recent_keystrokes.push(ev.keystroke.clone());
727                cx.notify();
728            })
729        })
730        .detach();
731        cx.on_keyboard_layout_change({
732            move |cx| {
733                window.update(cx, |_, _, cx| cx.notify()).ok();
734            }
735        })
736        .detach();
737
738        window
739            .update(cx, |view, window, cx| {
740                window.focus(&view.text_input.focus_handle(cx));
741                cx.activate(true);
742            })
743            .unwrap();
744    });
745}