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