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 let Some(cursor) = prepaint.cursor.take() {
471            cx.paint_quad(cursor);
472        }
473        self.input.update(cx, |input, _cx| {
474            input.last_layout = Some(line);
475            input.last_bounds = Some(bounds);
476        });
477    }
478}
479
480impl Render for TextInput {
481    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
482        div()
483            .flex()
484            .key_context("TextInput")
485            .track_focus(&self.focus_handle)
486            .cursor(CursorStyle::IBeam)
487            .on_action(cx.listener(Self::backspace))
488            .on_action(cx.listener(Self::delete))
489            .on_action(cx.listener(Self::left))
490            .on_action(cx.listener(Self::right))
491            .on_action(cx.listener(Self::select_left))
492            .on_action(cx.listener(Self::select_right))
493            .on_action(cx.listener(Self::select_all))
494            .on_action(cx.listener(Self::home))
495            .on_action(cx.listener(Self::end))
496            .on_action(cx.listener(Self::show_character_palette))
497            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
498            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
499            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
500            .on_mouse_move(cx.listener(Self::on_mouse_move))
501            .bg(rgb(0xeeeeee))
502            .size_full()
503            .line_height(px(30.))
504            .text_size(px(24.))
505            .child(
506                div()
507                    .h(px(30. + 4. * 2.))
508                    .w_full()
509                    .p(px(4.))
510                    .bg(white())
511                    .child(TextElement {
512                        input: cx.view().clone(),
513                    }),
514            )
515    }
516}
517
518impl FocusableView for TextInput {
519    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
520        self.focus_handle.clone()
521    }
522}
523
524struct InputExample {
525    text_input: View<TextInput>,
526    recent_keystrokes: Vec<Keystroke>,
527}
528
529impl InputExample {
530    fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
531        self.recent_keystrokes.clear();
532        self.text_input
533            .update(cx, |text_input, _cx| text_input.reset());
534        cx.notify();
535    }
536}
537
538impl Render for InputExample {
539    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
540        let num_keystrokes = self.recent_keystrokes.len();
541        div()
542            .bg(rgb(0xaaaaaa))
543            .flex()
544            .flex_col()
545            .size_full()
546            .child(
547                div()
548                    .bg(white())
549                    .border_b_1()
550                    .border_color(black())
551                    .flex()
552                    .flex_row()
553                    .justify_between()
554                    .child(format!("Keystrokes: {}", num_keystrokes))
555                    .child(
556                        div()
557                            .border_1()
558                            .border_color(black())
559                            .px_2()
560                            .bg(yellow())
561                            .child("Reset")
562                            .hover(|style| {
563                                style
564                                    .bg(yellow().blend(opaque_grey(0.5, 0.5)))
565                                    .cursor_pointer()
566                            })
567                            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
568                    ),
569            )
570            .child(self.text_input.clone())
571            .children(self.recent_keystrokes.iter().rev().map(|ks| {
572                format!(
573                    "{:} {}",
574                    ks,
575                    if let Some(ime_key) = ks.ime_key.as_ref() {
576                        format!("-> {}", ime_key)
577                    } else {
578                        "".to_owned()
579                    }
580                )
581            }))
582    }
583}
584
585fn main() {
586    App::new().run(|cx: &mut AppContext| {
587        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
588        cx.bind_keys([
589            KeyBinding::new("backspace", Backspace, None),
590            KeyBinding::new("delete", Delete, None),
591            KeyBinding::new("left", Left, None),
592            KeyBinding::new("right", Right, None),
593            KeyBinding::new("shift-left", SelectLeft, None),
594            KeyBinding::new("shift-right", SelectRight, None),
595            KeyBinding::new("cmd-a", SelectAll, None),
596            KeyBinding::new("home", Home, None),
597            KeyBinding::new("end", End, None),
598            KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
599        ]);
600        let window = cx
601            .open_window(
602                WindowOptions {
603                    window_bounds: Some(WindowBounds::Windowed(bounds)),
604                    ..Default::default()
605                },
606                |cx| {
607                    let text_input = cx.new_view(|cx| TextInput {
608                        focus_handle: cx.focus_handle(),
609                        content: "".into(),
610                        placeholder: "Type here...".into(),
611                        selected_range: 0..0,
612                        selection_reversed: false,
613                        marked_range: None,
614                        last_layout: None,
615                        last_bounds: None,
616                        is_selecting: false,
617                    });
618                    cx.new_view(|_| InputExample {
619                        text_input,
620                        recent_keystrokes: vec![],
621                    })
622                },
623            )
624            .unwrap();
625        cx.observe_keystrokes(move |ev, cx| {
626            window
627                .update(cx, |view, cx| {
628                    view.recent_keystrokes.push(ev.keystroke.clone());
629                    cx.notify();
630                })
631                .unwrap();
632        })
633        .detach();
634        window
635            .update(cx, |view, cx| {
636                cx.focus_view(&view.text_input);
637                cx.activate(true);
638            })
639            .unwrap();
640    });
641}