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