input.rs

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