input.rs

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