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}
 30
 31impl TextInput {
 32    fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
 33        if self.selected_range.is_empty() {
 34            self.move_to(self.previous_boundary(self.cursor_offset()), cx);
 35        } else {
 36            self.move_to(self.selected_range.start, cx)
 37        }
 38    }
 39
 40    fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
 41        if self.selected_range.is_empty() {
 42            self.move_to(self.next_boundary(self.selected_range.end), cx);
 43        } else {
 44            self.move_to(self.selected_range.end, cx)
 45        }
 46    }
 47
 48    fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
 49        self.select_to(self.previous_boundary(self.cursor_offset()), cx);
 50    }
 51
 52    fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
 53        self.select_to(self.next_boundary(self.cursor_offset()), cx);
 54    }
 55
 56    fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
 57        self.move_to(0, cx);
 58        self.select_to(self.content.len(), cx)
 59    }
 60
 61    fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
 62        self.move_to(0, cx);
 63    }
 64
 65    fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
 66        self.move_to(self.content.len(), cx);
 67    }
 68
 69    fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
 70        if self.selected_range.is_empty() {
 71            self.select_to(self.previous_boundary(self.cursor_offset()), cx)
 72        }
 73        self.replace_text_in_range(None, "", cx)
 74    }
 75
 76    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
 77        if self.selected_range.is_empty() {
 78            self.select_to(self.next_boundary(self.cursor_offset()), cx)
 79        }
 80        self.replace_text_in_range(None, "", cx)
 81    }
 82
 83    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
 84        cx.show_character_palette();
 85    }
 86
 87    fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
 88        self.selected_range = offset..offset;
 89        cx.notify()
 90    }
 91
 92    fn cursor_offset(&self) -> usize {
 93        if self.selection_reversed {
 94            self.selected_range.start
 95        } else {
 96            self.selected_range.end
 97        }
 98    }
 99
100    fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
101        if self.selection_reversed {
102            self.selected_range.start = offset
103        } else {
104            self.selected_range.end = offset
105        };
106        if self.selected_range.end < self.selected_range.start {
107            self.selection_reversed = !self.selection_reversed;
108            self.selected_range = self.selected_range.end..self.selected_range.start;
109        }
110        cx.notify()
111    }
112
113    fn offset_from_utf16(&self, offset: usize) -> usize {
114        let mut utf8_offset = 0;
115        let mut utf16_count = 0;
116
117        for ch in self.content.chars() {
118            if utf16_count >= offset {
119                break;
120            }
121            utf16_count += ch.len_utf16();
122            utf8_offset += ch.len_utf8();
123        }
124
125        utf8_offset
126    }
127
128    fn offset_to_utf16(&self, offset: usize) -> usize {
129        let mut utf16_offset = 0;
130        let mut utf8_count = 0;
131
132        for ch in self.content.chars() {
133            if utf8_count >= offset {
134                break;
135            }
136            utf8_count += ch.len_utf8();
137            utf16_offset += ch.len_utf16();
138        }
139
140        utf16_offset
141    }
142
143    fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
144        self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
145    }
146
147    fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
148        self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
149    }
150
151    fn previous_boundary(&self, offset: usize) -> usize {
152        self.content
153            .grapheme_indices(true)
154            .rev()
155            .find_map(|(idx, _)| (idx < offset).then_some(idx))
156            .unwrap_or(0)
157    }
158
159    fn next_boundary(&self, offset: usize) -> usize {
160        self.content
161            .grapheme_indices(true)
162            .find_map(|(idx, _)| (idx > offset).then_some(idx))
163            .unwrap_or(self.content.len())
164    }
165}
166
167impl ViewInputHandler for TextInput {
168    fn text_for_range(
169        &mut self,
170        range_utf16: Range<usize>,
171        _cx: &mut ViewContext<Self>,
172    ) -> Option<String> {
173        let range = self.range_from_utf16(&range_utf16);
174        Some(self.content[range].to_string())
175    }
176
177    fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
178        Some(self.range_to_utf16(&self.selected_range))
179    }
180
181    fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
182        self.marked_range
183            .as_ref()
184            .map(|range| self.range_to_utf16(range))
185    }
186
187    fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
188        self.marked_range = None;
189    }
190
191    fn replace_text_in_range(
192        &mut self,
193        range_utf16: Option<Range<usize>>,
194        new_text: &str,
195        cx: &mut ViewContext<Self>,
196    ) {
197        let range = range_utf16
198            .as_ref()
199            .map(|range_utf16| self.range_from_utf16(range_utf16))
200            .or(self.marked_range.clone())
201            .unwrap_or(self.selected_range.clone());
202
203        self.content =
204            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
205                .into();
206        self.selected_range = range.start + new_text.len()..range.start + new_text.len();
207        self.marked_range.take();
208        cx.notify();
209    }
210
211    fn replace_and_mark_text_in_range(
212        &mut self,
213        range_utf16: Option<Range<usize>>,
214        new_text: &str,
215        new_selected_range_utf16: Option<Range<usize>>,
216        cx: &mut ViewContext<Self>,
217    ) {
218        let range = range_utf16
219            .as_ref()
220            .map(|range_utf16| self.range_from_utf16(range_utf16))
221            .or(self.marked_range.clone())
222            .unwrap_or(self.selected_range.clone());
223
224        self.content =
225            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
226                .into();
227        self.marked_range = Some(range.start..range.start + new_text.len());
228        self.selected_range = new_selected_range_utf16
229            .as_ref()
230            .map(|range_utf16| self.range_from_utf16(range_utf16))
231            .map(|new_range| new_range.start + range.start..new_range.end + range.end)
232            .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
233
234        cx.notify();
235    }
236
237    fn bounds_for_range(
238        &mut self,
239        range_utf16: Range<usize>,
240        bounds: Bounds<Pixels>,
241        _cx: &mut ViewContext<Self>,
242    ) -> Option<Bounds<Pixels>> {
243        let Some(last_layout) = self.last_layout.as_ref() else {
244            return None;
245        };
246        let range = self.range_from_utf16(&range_utf16);
247        Some(Bounds::from_corners(
248            point(
249                bounds.left() + last_layout.x_for_index(range.start),
250                bounds.top(),
251            ),
252            point(
253                bounds.left() + last_layout.x_for_index(range.end),
254                bounds.bottom(),
255            ),
256        ))
257    }
258}
259
260struct TextElement {
261    input: View<TextInput>,
262}
263
264struct PrepaintState {
265    line: Option<ShapedLine>,
266    cursor: Option<PaintQuad>,
267    selection: Option<PaintQuad>,
268}
269
270impl IntoElement for TextElement {
271    type Element = Self;
272
273    fn into_element(self) -> Self::Element {
274        self
275    }
276}
277
278impl Element for TextElement {
279    type RequestLayoutState = ();
280
281    type PrepaintState = PrepaintState;
282
283    fn id(&self) -> Option<ElementId> {
284        None
285    }
286
287    fn request_layout(
288        &mut self,
289        _id: Option<&GlobalElementId>,
290        cx: &mut WindowContext,
291    ) -> (LayoutId, Self::RequestLayoutState) {
292        let mut style = Style::default();
293        style.size.width = relative(1.).into();
294        style.size.height = cx.line_height().into();
295        (cx.request_layout(style, []), ())
296    }
297
298    fn prepaint(
299        &mut self,
300        _id: Option<&GlobalElementId>,
301        bounds: Bounds<Pixels>,
302        _request_layout: &mut Self::RequestLayoutState,
303        cx: &mut WindowContext,
304    ) -> Self::PrepaintState {
305        let input = self.input.read(cx);
306        let content = input.content.clone();
307        let selected_range = input.selected_range.clone();
308        let cursor = input.cursor_offset();
309        let style = cx.text_style();
310        let run = TextRun {
311            len: input.content.len(),
312            font: style.font(),
313            color: style.color,
314            background_color: None,
315            underline: None,
316            strikethrough: None,
317        };
318        let runs = if let Some(marked_range) = input.marked_range.as_ref() {
319            vec![
320                TextRun {
321                    len: marked_range.start,
322                    ..run.clone()
323                },
324                TextRun {
325                    len: marked_range.end - marked_range.start,
326                    underline: Some(UnderlineStyle {
327                        color: Some(run.color),
328                        thickness: px(1.0),
329                        wavy: false,
330                    }),
331                    ..run.clone()
332                },
333                TextRun {
334                    len: input.content.len() - marked_range.end,
335                    ..run.clone()
336                },
337            ]
338            .into_iter()
339            .filter(|run| run.len > 0)
340            .collect()
341        } else {
342            vec![run]
343        };
344
345        let font_size = style.font_size.to_pixels(cx.rem_size());
346        let line = cx
347            .text_system()
348            .shape_line(content, font_size, &runs)
349            .unwrap();
350
351        let cursor_pos = line.x_for_index(cursor);
352        let (selection, cursor) = if selected_range.is_empty() {
353            (
354                None,
355                Some(fill(
356                    Bounds::new(
357                        point(bounds.left() + cursor_pos, bounds.top()),
358                        size(px(2.), bounds.bottom() - bounds.top()),
359                    ),
360                    gpui::blue(),
361                )),
362            )
363        } else {
364            (
365                Some(fill(
366                    Bounds::from_corners(
367                        point(
368                            bounds.left() + line.x_for_index(selected_range.start),
369                            bounds.top(),
370                        ),
371                        point(
372                            bounds.left() + line.x_for_index(selected_range.end),
373                            bounds.bottom(),
374                        ),
375                    ),
376                    rgba(0x3311FF30),
377                )),
378                None,
379            )
380        };
381        PrepaintState {
382            line: Some(line),
383            cursor,
384            selection,
385        }
386    }
387
388    fn paint(
389        &mut self,
390        _id: Option<&GlobalElementId>,
391        bounds: Bounds<Pixels>,
392        _request_layout: &mut Self::RequestLayoutState,
393        prepaint: &mut Self::PrepaintState,
394        cx: &mut WindowContext,
395    ) {
396        let focus_handle = self.input.read(cx).focus_handle.clone();
397        cx.handle_input(
398            &focus_handle,
399            ElementInputHandler::new(bounds, self.input.clone()),
400        );
401        if let Some(selection) = prepaint.selection.take() {
402            cx.paint_quad(selection)
403        }
404        let line = prepaint.line.take().unwrap();
405        line.paint(bounds.origin, cx.line_height(), cx).unwrap();
406
407        if let Some(cursor) = prepaint.cursor.take() {
408            cx.paint_quad(cursor);
409        }
410        self.input.update(cx, |input, _cx| {
411            input.last_layout = Some(line);
412        });
413    }
414}
415
416impl Render for TextInput {
417    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
418        div()
419            .flex()
420            .key_context("TextInput")
421            .track_focus(&self.focus_handle)
422            .on_action(cx.listener(Self::backspace))
423            .on_action(cx.listener(Self::delete))
424            .on_action(cx.listener(Self::left))
425            .on_action(cx.listener(Self::right))
426            .on_action(cx.listener(Self::select_left))
427            .on_action(cx.listener(Self::select_right))
428            .on_action(cx.listener(Self::select_all))
429            .on_action(cx.listener(Self::home))
430            .on_action(cx.listener(Self::end))
431            .on_action(cx.listener(Self::show_character_palette))
432            .bg(rgb(0xeeeeee))
433            .size_full()
434            .line_height(px(30.))
435            .text_size(px(24.))
436            .child(
437                div()
438                    .h(px(30. + 4. * 2.))
439                    .w_full()
440                    .p(px(4.))
441                    .bg(white())
442                    .child(TextElement {
443                        input: cx.view().clone(),
444                    }),
445            )
446    }
447}
448
449fn main() {
450    App::new().run(|cx: &mut AppContext| {
451        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
452        cx.bind_keys([
453            KeyBinding::new("backspace", Backspace, None),
454            KeyBinding::new("delete", Delete, None),
455            KeyBinding::new("left", Left, None),
456            KeyBinding::new("right", Right, None),
457            KeyBinding::new("shift-left", SelectLeft, None),
458            KeyBinding::new("shift-right", SelectRight, None),
459            KeyBinding::new("cmd-a", SelectAll, None),
460            KeyBinding::new("home", Home, None),
461            KeyBinding::new("end", End, None),
462            KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
463        ]);
464        let window = cx
465            .open_window(
466                WindowOptions {
467                    window_bounds: Some(WindowBounds::Windowed(bounds)),
468                    ..Default::default()
469                },
470                |cx| {
471                    cx.new_view(|cx| TextInput {
472                        focus_handle: cx.focus_handle(),
473                        content: "".into(),
474                        selected_range: 0..0,
475                        selection_reversed: false,
476                        marked_range: None,
477                        last_layout: None,
478                    })
479                },
480            )
481            .unwrap();
482        window
483            .update(cx, |view, cx| {
484                view.focus_handle.focus(cx);
485                cx.activate(true)
486            })
487            .unwrap();
488    });
489}