input.rs

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