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
408    type PrepaintState = PrepaintState;
409
410    fn id(&self) -> Option<ElementId> {
411        None
412    }
413
414    fn request_layout(
415        &mut self,
416        _id: Option<&GlobalElementId>,
417        window: &mut Window,
418        cx: &mut App,
419    ) -> (LayoutId, Self::RequestLayoutState) {
420        let mut style = Style::default();
421        style.size.width = relative(1.).into();
422        style.size.height = window.line_height().into();
423        (window.request_layout(style, [], cx), ())
424    }
425
426    fn prepaint(
427        &mut self,
428        _id: Option<&GlobalElementId>,
429        bounds: Bounds<Pixels>,
430        _request_layout: &mut Self::RequestLayoutState,
431        window: &mut Window,
432        cx: &mut App,
433    ) -> Self::PrepaintState {
434        let input = self.input.read(cx);
435        let content = input.content.clone();
436        let selected_range = input.selected_range.clone();
437        let cursor = input.cursor_offset();
438        let style = window.text_style();
439
440        let (display_text, text_color) = if content.is_empty() {
441            (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
442        } else {
443            (content.clone(), style.color)
444        };
445
446        let run = TextRun {
447            len: display_text.len(),
448            font: style.font(),
449            color: text_color,
450            background_color: None,
451            underline: None,
452            strikethrough: None,
453        };
454        let runs = if let Some(marked_range) = input.marked_range.as_ref() {
455            vec![
456                TextRun {
457                    len: marked_range.start,
458                    ..run.clone()
459                },
460                TextRun {
461                    len: marked_range.end - marked_range.start,
462                    underline: Some(UnderlineStyle {
463                        color: Some(run.color),
464                        thickness: px(1.0),
465                        wavy: false,
466                    }),
467                    ..run.clone()
468                },
469                TextRun {
470                    len: display_text.len() - marked_range.end,
471                    ..run.clone()
472                },
473            ]
474            .into_iter()
475            .filter(|run| run.len > 0)
476            .collect()
477        } else {
478            vec![run]
479        };
480
481        let font_size = style.font_size.to_pixels(window.rem_size());
482        let line = window
483            .text_system()
484            .shape_line(display_text, font_size, &runs);
485
486        let cursor_pos = line.x_for_index(cursor);
487        let (selection, cursor) = if selected_range.is_empty() {
488            (
489                None,
490                Some(fill(
491                    Bounds::new(
492                        point(bounds.left() + cursor_pos, bounds.top()),
493                        size(px(2.), bounds.bottom() - bounds.top()),
494                    ),
495                    gpui::blue(),
496                )),
497            )
498        } else {
499            (
500                Some(fill(
501                    Bounds::from_corners(
502                        point(
503                            bounds.left() + line.x_for_index(selected_range.start),
504                            bounds.top(),
505                        ),
506                        point(
507                            bounds.left() + line.x_for_index(selected_range.end),
508                            bounds.bottom(),
509                        ),
510                    ),
511                    rgba(0x3311ff30),
512                )),
513                None,
514            )
515        };
516        PrepaintState {
517            line: Some(line),
518            cursor,
519            selection,
520        }
521    }
522
523    fn paint(
524        &mut self,
525        _id: Option<&GlobalElementId>,
526        bounds: Bounds<Pixels>,
527        _request_layout: &mut Self::RequestLayoutState,
528        prepaint: &mut Self::PrepaintState,
529        window: &mut Window,
530        cx: &mut App,
531    ) {
532        let focus_handle = self.input.read(cx).focus_handle.clone();
533        window.handle_input(
534            &focus_handle,
535            ElementInputHandler::new(bounds, self.input.clone()),
536            cx,
537        );
538        if let Some(selection) = prepaint.selection.take() {
539            window.paint_quad(selection)
540        }
541        let line = prepaint.line.take().unwrap();
542        line.paint(bounds.origin, window.line_height(), window, cx)
543            .unwrap();
544
545        if focus_handle.is_focused(window) {
546            if let Some(cursor) = prepaint.cursor.take() {
547                window.paint_quad(cursor);
548            }
549        }
550
551        self.input.update(cx, |input, _cx| {
552            input.last_layout = Some(line);
553            input.last_bounds = Some(bounds);
554        });
555    }
556}
557
558impl Render for TextInput {
559    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
560        div()
561            .flex()
562            .key_context("TextInput")
563            .track_focus(&self.focus_handle(cx))
564            .cursor(CursorStyle::IBeam)
565            .on_action(cx.listener(Self::backspace))
566            .on_action(cx.listener(Self::delete))
567            .on_action(cx.listener(Self::left))
568            .on_action(cx.listener(Self::right))
569            .on_action(cx.listener(Self::select_left))
570            .on_action(cx.listener(Self::select_right))
571            .on_action(cx.listener(Self::select_all))
572            .on_action(cx.listener(Self::home))
573            .on_action(cx.listener(Self::end))
574            .on_action(cx.listener(Self::show_character_palette))
575            .on_action(cx.listener(Self::paste))
576            .on_action(cx.listener(Self::cut))
577            .on_action(cx.listener(Self::copy))
578            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
579            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
580            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
581            .on_mouse_move(cx.listener(Self::on_mouse_move))
582            .bg(rgb(0xeeeeee))
583            .line_height(px(30.))
584            .text_size(px(24.))
585            .child(
586                div()
587                    .h(px(30. + 4. * 2.))
588                    .w_full()
589                    .p(px(4.))
590                    .bg(white())
591                    .child(TextElement {
592                        input: cx.entity().clone(),
593                    }),
594            )
595    }
596}
597
598impl Focusable for TextInput {
599    fn focus_handle(&self, _: &App) -> FocusHandle {
600        self.focus_handle.clone()
601    }
602}
603
604struct InputExample {
605    text_input: Entity<TextInput>,
606    recent_keystrokes: Vec<Keystroke>,
607    focus_handle: FocusHandle,
608}
609
610impl Focusable for InputExample {
611    fn focus_handle(&self, _: &App) -> FocusHandle {
612        self.focus_handle.clone()
613    }
614}
615
616impl InputExample {
617    fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
618        self.recent_keystrokes.clear();
619        self.text_input
620            .update(cx, |text_input, _cx| text_input.reset());
621        cx.notify();
622    }
623}
624
625impl Render for InputExample {
626    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
627        div()
628            .bg(rgb(0xaaaaaa))
629            .track_focus(&self.focus_handle(cx))
630            .flex()
631            .flex_col()
632            .size_full()
633            .child(
634                div()
635                    .bg(white())
636                    .border_b_1()
637                    .border_color(black())
638                    .flex()
639                    .flex_row()
640                    .justify_between()
641                    .child(format!("Keyboard {}", cx.keyboard_layout().name()))
642                    .child(
643                        div()
644                            .border_1()
645                            .border_color(black())
646                            .px_2()
647                            .bg(yellow())
648                            .child("Reset")
649                            .hover(|style| {
650                                style
651                                    .bg(yellow().blend(opaque_grey(0.5, 0.5)))
652                                    .cursor_pointer()
653                            })
654                            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
655                    ),
656            )
657            .child(self.text_input.clone())
658            .children(self.recent_keystrokes.iter().rev().map(|ks| {
659                format!(
660                    "{:} {}",
661                    ks.unparse(),
662                    if let Some(key_char) = ks.key_char.as_ref() {
663                        format!("-> {:?}", key_char)
664                    } else {
665                        "".to_owned()
666                    }
667                )
668            }))
669    }
670}
671
672fn main() {
673    Application::new().run(|cx: &mut App| {
674        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
675        cx.bind_keys([
676            KeyBinding::new("backspace", Backspace, None),
677            KeyBinding::new("delete", Delete, None),
678            KeyBinding::new("left", Left, None),
679            KeyBinding::new("right", Right, None),
680            KeyBinding::new("shift-left", SelectLeft, None),
681            KeyBinding::new("shift-right", SelectRight, None),
682            KeyBinding::new("cmd-a", SelectAll, None),
683            KeyBinding::new("cmd-v", Paste, None),
684            KeyBinding::new("cmd-c", Copy, None),
685            KeyBinding::new("cmd-x", Cut, None),
686            KeyBinding::new("home", Home, None),
687            KeyBinding::new("end", End, None),
688            KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
689        ]);
690
691        let window = cx
692            .open_window(
693                WindowOptions {
694                    window_bounds: Some(WindowBounds::Windowed(bounds)),
695                    ..Default::default()
696                },
697                |_, cx| {
698                    let text_input = cx.new(|cx| TextInput {
699                        focus_handle: cx.focus_handle(),
700                        content: "".into(),
701                        placeholder: "Type here...".into(),
702                        selected_range: 0..0,
703                        selection_reversed: false,
704                        marked_range: None,
705                        last_layout: None,
706                        last_bounds: None,
707                        is_selecting: false,
708                    });
709                    cx.new(|cx| InputExample {
710                        text_input,
711                        recent_keystrokes: vec![],
712                        focus_handle: cx.focus_handle(),
713                    })
714                },
715            )
716            .unwrap();
717        let view = window.update(cx, |_, _, cx| cx.entity()).unwrap();
718        cx.observe_keystrokes(move |ev, _, cx| {
719            view.update(cx, |view, cx| {
720                view.recent_keystrokes.push(ev.keystroke.clone());
721                cx.notify();
722            })
723        })
724        .detach();
725        cx.on_keyboard_layout_change({
726            move |cx| {
727                window.update(cx, |_, _, cx| cx.notify()).ok();
728            }
729        })
730        .detach();
731
732        window
733            .update(cx, |view, window, cx| {
734                window.focus(&view.text_input.focus_handle(cx));
735                cx.activate(true);
736            })
737            .unwrap();
738    });
739}