input.rs

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