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(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
229        Some(self.range_to_utf16(&self.selected_range))
230    }
231
232    fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
233        self.marked_range
234            .as_ref()
235            .map(|range| self.range_to_utf16(range))
236    }
237
238    fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
239        self.marked_range = None;
240    }
241
242    fn replace_text_in_range(
243        &mut self,
244        range_utf16: Option<Range<usize>>,
245        new_text: &str,
246        cx: &mut ViewContext<Self>,
247    ) {
248        let range = range_utf16
249            .as_ref()
250            .map(|range_utf16| self.range_from_utf16(range_utf16))
251            .or(self.marked_range.clone())
252            .unwrap_or(self.selected_range.clone());
253
254        self.content =
255            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
256                .into();
257        self.selected_range = range.start + new_text.len()..range.start + new_text.len();
258        self.marked_range.take();
259        cx.notify();
260    }
261
262    fn replace_and_mark_text_in_range(
263        &mut self,
264        range_utf16: Option<Range<usize>>,
265        new_text: &str,
266        new_selected_range_utf16: Option<Range<usize>>,
267        cx: &mut ViewContext<Self>,
268    ) {
269        let range = range_utf16
270            .as_ref()
271            .map(|range_utf16| self.range_from_utf16(range_utf16))
272            .or(self.marked_range.clone())
273            .unwrap_or(self.selected_range.clone());
274
275        self.content =
276            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
277                .into();
278        self.marked_range = Some(range.start..range.start + new_text.len());
279        self.selected_range = new_selected_range_utf16
280            .as_ref()
281            .map(|range_utf16| self.range_from_utf16(range_utf16))
282            .map(|new_range| new_range.start + range.start..new_range.end + range.end)
283            .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
284
285        cx.notify();
286    }
287
288    fn bounds_for_range(
289        &mut self,
290        range_utf16: Range<usize>,
291        bounds: Bounds<Pixels>,
292        _cx: &mut ViewContext<Self>,
293    ) -> Option<Bounds<Pixels>> {
294        let Some(last_layout) = self.last_layout.as_ref() else {
295            return None;
296        };
297        let range = self.range_from_utf16(&range_utf16);
298        Some(Bounds::from_corners(
299            point(
300                bounds.left() + last_layout.x_for_index(range.start),
301                bounds.top(),
302            ),
303            point(
304                bounds.left() + last_layout.x_for_index(range.end),
305                bounds.bottom(),
306            ),
307        ))
308    }
309}
310
311struct TextElement {
312    input: View<TextInput>,
313}
314
315struct PrepaintState {
316    line: Option<ShapedLine>,
317    cursor: Option<PaintQuad>,
318    selection: Option<PaintQuad>,
319}
320
321impl IntoElement for TextElement {
322    type Element = Self;
323
324    fn into_element(self) -> Self::Element {
325        self
326    }
327}
328
329impl Element for TextElement {
330    type RequestLayoutState = ();
331
332    type PrepaintState = PrepaintState;
333
334    fn id(&self) -> Option<ElementId> {
335        None
336    }
337
338    fn request_layout(
339        &mut self,
340        _id: Option<&GlobalElementId>,
341        cx: &mut WindowContext,
342    ) -> (LayoutId, Self::RequestLayoutState) {
343        let mut style = Style::default();
344        style.size.width = relative(1.).into();
345        style.size.height = cx.line_height().into();
346        (cx.request_layout(style, []), ())
347    }
348
349    fn prepaint(
350        &mut self,
351        _id: Option<&GlobalElementId>,
352        bounds: Bounds<Pixels>,
353        _request_layout: &mut Self::RequestLayoutState,
354        cx: &mut WindowContext,
355    ) -> Self::PrepaintState {
356        let input = self.input.read(cx);
357        let content = input.content.clone();
358        let selected_range = input.selected_range.clone();
359        let cursor = input.cursor_offset();
360        let style = cx.text_style();
361
362        let (display_text, text_color) = if content.is_empty() {
363            (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
364        } else {
365            (content.clone(), style.color)
366        };
367
368        let run = TextRun {
369            len: display_text.len(),
370            font: style.font(),
371            color: text_color,
372            background_color: None,
373            underline: None,
374            strikethrough: None,
375        };
376        let runs = if let Some(marked_range) = input.marked_range.as_ref() {
377            vec![
378                TextRun {
379                    len: marked_range.start,
380                    ..run.clone()
381                },
382                TextRun {
383                    len: marked_range.end - marked_range.start,
384                    underline: Some(UnderlineStyle {
385                        color: Some(run.color),
386                        thickness: px(1.0),
387                        wavy: false,
388                    }),
389                    ..run.clone()
390                },
391                TextRun {
392                    len: display_text.len() - marked_range.end,
393                    ..run.clone()
394                },
395            ]
396            .into_iter()
397            .filter(|run| run.len > 0)
398            .collect()
399        } else {
400            vec![run]
401        };
402
403        let font_size = style.font_size.to_pixels(cx.rem_size());
404        let line = cx
405            .text_system()
406            .shape_line(display_text, font_size, &runs)
407            .unwrap();
408
409        let cursor_pos = line.x_for_index(cursor);
410        let (selection, cursor) = if selected_range.is_empty() {
411            (
412                None,
413                Some(fill(
414                    Bounds::new(
415                        point(bounds.left() + cursor_pos, bounds.top()),
416                        size(px(2.), bounds.bottom() - bounds.top()),
417                    ),
418                    gpui::blue(),
419                )),
420            )
421        } else {
422            (
423                Some(fill(
424                    Bounds::from_corners(
425                        point(
426                            bounds.left() + line.x_for_index(selected_range.start),
427                            bounds.top(),
428                        ),
429                        point(
430                            bounds.left() + line.x_for_index(selected_range.end),
431                            bounds.bottom(),
432                        ),
433                    ),
434                    rgba(0x3311FF30),
435                )),
436                None,
437            )
438        };
439        PrepaintState {
440            line: Some(line),
441            cursor,
442            selection,
443        }
444    }
445
446    fn paint(
447        &mut self,
448        _id: Option<&GlobalElementId>,
449        bounds: Bounds<Pixels>,
450        _request_layout: &mut Self::RequestLayoutState,
451        prepaint: &mut Self::PrepaintState,
452        cx: &mut WindowContext,
453    ) {
454        let focus_handle = self.input.read(cx).focus_handle.clone();
455        cx.handle_input(
456            &focus_handle,
457            ElementInputHandler::new(bounds, self.input.clone()),
458        );
459        if let Some(selection) = prepaint.selection.take() {
460            cx.paint_quad(selection)
461        }
462        let line = prepaint.line.take().unwrap();
463        line.paint(bounds.origin, cx.line_height(), cx).unwrap();
464
465        if let Some(cursor) = prepaint.cursor.take() {
466            cx.paint_quad(cursor);
467        }
468        self.input.update(cx, |input, _cx| {
469            input.last_layout = Some(line);
470            input.last_bounds = Some(bounds);
471        });
472    }
473}
474
475impl Render for TextInput {
476    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
477        div()
478            .flex()
479            .key_context("TextInput")
480            .track_focus(&self.focus_handle)
481            .cursor(CursorStyle::IBeam)
482            .on_action(cx.listener(Self::backspace))
483            .on_action(cx.listener(Self::delete))
484            .on_action(cx.listener(Self::left))
485            .on_action(cx.listener(Self::right))
486            .on_action(cx.listener(Self::select_left))
487            .on_action(cx.listener(Self::select_right))
488            .on_action(cx.listener(Self::select_all))
489            .on_action(cx.listener(Self::home))
490            .on_action(cx.listener(Self::end))
491            .on_action(cx.listener(Self::show_character_palette))
492            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
493            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
494            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
495            .on_mouse_move(cx.listener(Self::on_mouse_move))
496            .bg(rgb(0xeeeeee))
497            .size_full()
498            .line_height(px(30.))
499            .text_size(px(24.))
500            .child(
501                div()
502                    .h(px(30. + 4. * 2.))
503                    .w_full()
504                    .p(px(4.))
505                    .bg(white())
506                    .child(TextElement {
507                        input: cx.view().clone(),
508                    }),
509            )
510    }
511}
512
513impl FocusableView for TextInput {
514    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
515        self.focus_handle.clone()
516    }
517}
518
519struct InputExample {
520    text_input: View<TextInput>,
521    recent_keystrokes: Vec<Keystroke>,
522}
523
524impl InputExample {
525    fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
526        self.recent_keystrokes.clear();
527        self.text_input
528            .update(cx, |text_input, _cx| text_input.reset());
529        cx.notify();
530    }
531}
532
533impl Render for InputExample {
534    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
535        let num_keystrokes = self.recent_keystrokes.len();
536        div()
537            .bg(rgb(0xaaaaaa))
538            .flex()
539            .flex_col()
540            .size_full()
541            .child(
542                div()
543                    .bg(white())
544                    .border_b_1()
545                    .border_color(black())
546                    .flex()
547                    .flex_row()
548                    .justify_between()
549                    .child(format!("Keystrokes: {}", num_keystrokes))
550                    .child(
551                        div()
552                            .border_1()
553                            .border_color(black())
554                            .px_2()
555                            .bg(yellow())
556                            .child("Reset")
557                            .hover(|style| {
558                                style
559                                    .bg(yellow().blend(opaque_grey(0.5, 0.5)))
560                                    .cursor_pointer()
561                            })
562                            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
563                    ),
564            )
565            .child(self.text_input.clone())
566            .children(self.recent_keystrokes.iter().rev().map(|ks| {
567                format!(
568                    "{:} {}",
569                    ks,
570                    if let Some(ime_key) = ks.ime_key.as_ref() {
571                        format!("-> {}", ime_key)
572                    } else {
573                        "".to_owned()
574                    }
575                )
576            }))
577    }
578}
579
580fn main() {
581    App::new().run(|cx: &mut AppContext| {
582        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
583        cx.bind_keys([
584            KeyBinding::new("backspace", Backspace, None),
585            KeyBinding::new("delete", Delete, None),
586            KeyBinding::new("left", Left, None),
587            KeyBinding::new("right", Right, None),
588            KeyBinding::new("shift-left", SelectLeft, None),
589            KeyBinding::new("shift-right", SelectRight, None),
590            KeyBinding::new("cmd-a", SelectAll, None),
591            KeyBinding::new("home", Home, None),
592            KeyBinding::new("end", End, None),
593            KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
594        ]);
595        let window = cx
596            .open_window(
597                WindowOptions {
598                    window_bounds: Some(WindowBounds::Windowed(bounds)),
599                    ..Default::default()
600                },
601                |cx| {
602                    let text_input = cx.new_view(|cx| TextInput {
603                        focus_handle: cx.focus_handle(),
604                        content: "".into(),
605                        placeholder: "Type here...".into(),
606                        selected_range: 0..0,
607                        selection_reversed: false,
608                        marked_range: None,
609                        last_layout: None,
610                        last_bounds: None,
611                        is_selecting: false,
612                    });
613                    cx.new_view(|_| InputExample {
614                        text_input,
615                        recent_keystrokes: vec![],
616                    })
617                },
618            )
619            .unwrap();
620        cx.observe_keystrokes(move |ev, cx| {
621            window
622                .update(cx, |view, cx| {
623                    view.recent_keystrokes.push(ev.keystroke.clone());
624                    cx.notify();
625                })
626                .unwrap();
627        })
628        .detach();
629        window
630            .update(cx, |view, cx| {
631                cx.focus_view(&view.text_input);
632                cx.activate(true);
633            })
634            .unwrap();
635    });
636}