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