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