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